Building modules

Build a module for Muon Insight

Write your own modules in PowerShell or Python: manifest, context, results, safety and publishing.

For Muon Insight 0.26.0. This guide also ships inside the app under Help and Documentation.

This guide assumes you can edit JSON and either PowerShell or Python. You do not need to understand the toolkit’s UI code, task engine, or packaging system before you begin.

The safest way to build a module is:

  1. Pick the example closest to what you want.
  2. Copy its entire folder.
  3. Rename the module in module.json.
  4. Replace the small, clearly marked example logic.
  5. Run it in the toolkit and inspect every result tab.

If this is your first module, start with Your first module in ten minutes. The later sections explain why the pieces work and what is required for production use.

Guide map

  • Start here: The idea in one minute, Your first module in ten minutes, and Choose the right module type.
  • Define the contract: Understand the three files, Complete manifest reference, and Parameters and toolkit-generated controls.
  • Write the worker: The execution context, Toolkit interaction API, Result contract, and Minimal complete entry-point patterns.
  • Specialized designs: Safe actions, bounded log collectors, remote support, external integrations, and pack compatibility.
  • Release confidently: Failure handling, testing, publication, and the copyable example library.

Developers who already understand the folder and manifest can jump directly to Toolkit interaction API for the complete PowerShell and Python interface reference.


The idea in one minute

A module is one folder containing at least two files:

my-module/
|-- module.json   # Tells the toolkit what the module is allowed to do
|-- main.ps1      # Or main.py: performs the work and returns a result
`-- HELP.md       # Explains the tool to technicians

The toolkit—not the module—handles the window, target selection, parameter controls, confirmation dialogs, permissions checks, task history, cancellation request, and result tabs.

Your script receives the path to a temporary JSON execution context. It reads approved inputs from that file and writes one structured result. It must never create its own dialog box.

Technician chooses a module
        |
        v
Toolkit validates target, permissions, confirmation, and parameters
        |
        v
Toolkit starts main.ps1 or main.py with a context-file path
        |
        v
Module collects evidence or performs its declared action
        |
        v
Module writes one normalized result
        |
        v
Toolkit renders Summary, Tests, Findings, Files, and Raw Output

Your first module in ten minutes

This walkthrough creates a read-only PowerShell diagnostic.

1. Copy the diagnostic example

From the project root:

Copy-Item -Recurse `
  repository-examples\modules\diagnostic-powershell `
  modules\my-computer-check

Keep all three files together. Do not copy only main.ps1.

2. Give it a permanent identity

Open modules\my-computer-check\module.json and change these values:

{
  "id": "yourteam.my-computer-check",
  "name": "My Computer Check",
  "version": "1.0.0",
  "description": "Checks the computer condition my support team cares about.",
  "author": "Your Team"
}

The id is permanent. Use lowercase words separated by dots or hyphens. Do not include a hostname, person, date, or version in it.

3. Replace the example check

Open main.ps1. Search for this comment:

# EXAMPLE LOGIC: replace this block with your own read-only query.

Replace only that section at first. Leave context loading, progress, cancellation, tests, findings, error handling, and Complete-MuonModule intact until the new module runs successfully.

4. Make the help article specific

Open HELP.md. Explain:

  • the symptom that should lead a technician to this tool;
  • what the module checks;
  • how to interpret a warning;
  • what the module cannot see in a remote session;
  • the safest follow-up step.

5. Start the source application

python -m muon_insight

Open Troubleshooting Modules, search for the new name, and run it against This computer.

6. Inspect the result like a technician

Before writing more code, confirm:

  • Summary says what completed and whether attention is needed.
  • Test Results contains one readable row per check.
  • Findings explains the important condition and a safe next step.
  • Raw Output contains structured JSON and no passwords, tokens, or unnecessary personal data.
  • Cancelling during a longer operation returns cancelled, not a crash.

That is a complete first development loop.


Choose the right module type

Start with the simplest type that matches the job. Do not use an action for a read-only check.

Type Use it for Technician-facing behavior Copy this example
diagnostic Collecting and evaluating current device evidence Troubleshooting Modules diagnostic-powershell
action Making an intentional change Troubleshooting Modules with confirmation action-powershell
log_collector Creating a bounded support archive Log Collection log-collector-powershell
analyzer Applying deterministic logic to data Troubleshooting Modules analyzer-python
integration Querying an approved external service Troubleshooting Modules integration-python
report_generator Creating a purpose-built report attachment Host-extension contract report-generator-python
connection_test Testing a named endpoint or transport Host-extension contract connection-test-python
dashboard_provider Returning compact dashboard-card data Host-extension contract dashboard-provider-python

The last three types have valid execution and result contracts, but the current application does not automatically place them on the Reports, Connection Tests, or Dashboard pages. They are included so a host-extension developer has a correct starting contract. Do not promise automatic UI placement until the corresponding host registration is implemented.

See Module example library in Help and Documentation for a description of every example folder.

Understand the three files

module.json: the promise

The manifest is a promise between your module and the toolkit. The toolkit trusts it when deciding where the module appears, which targets are allowed, whether confirmation is required, and what input controls to show.

The most important fields are:

Field Plain-language meaning
id Permanent machine-readable name; never reuse it for a different tool
name Short title shown to technicians
description One or two sentences explaining the outcome
version Semantic version such as 1.2.0
module_type One type from the table above
risk_level Potential impact, not code complexity
supports_local The script truly works against This computer
supports_remote The script truly works for the selected remote target
allowed_transports How the current host can actually execute it
requires_confirmation The technician must approve immediately before execution
parameters Validated inputs that become UI controls
help_file Same-folder Markdown article, normally HELP.md

Do not mark a planned feature as supported. supports_remote: true means you have implemented and tested the remote path now.

main.ps1 or main.py: the worker

The entry point should do only module work. It must:

  1. Load the supplied context.
  2. Read only declared parameters and approved integration settings.
  3. Report useful progress for operations that take time.
  4. Check cancellation between expensive steps.
  5. Collect bounded evidence or perform the exact confirmed change.
  6. Return one result, including failures.

It must not import the toolkit UI, display prompts, ask for credentials, start an interactive shell, or write files outside approved context paths.

HELP.md: the technician’s instructions

The toolkit generates identity, parameter, target, and safety sections automatically. Use HELP.md for the knowledge only your tool’s author can provide: symptoms, evidence sources, interpretation, limitations, and follow-up actions.

Declare it in the manifest:

"help_file": "HELP.md"

See Help article authoring for a copyable article template.


The execution context, in plain language

The toolkit calls your script with exactly one argument: the path to a JSON file. Always load that file through the SDK.

PowerShell:

param([Parameter(Mandatory = $true)][string]$ContextPath)
$preview = Get-Content -Raw -LiteralPath $ContextPath | ConvertFrom-Json
Import-Module ([string]$preview.paths.sdk_powershell) -Force
$context = Import-MuonContext -ContextPath $ContextPath

Python:

from muon_insight.module_sdk import ModuleContext

context = ModuleContext.from_argv()

The fields you will use most often are:

Context value What it contains
task_id Unique ID for this run
module_id and module_version Identity copied from the manifest
target.target_name Selected computer
target.is_local Whether the selected target is this computer
parameters Validated values from manifest-generated controls
execution.transport local, winrm, or another approved transport
paths.temporary Task working directory; disposable
paths.artifacts Approved location for returned attachments
paths.output Required result JSON path
paths.cancellation File whose existence means stop safely
paths.sdk_powershell Staged PowerShell SDK path
integrations Approved non-secret integration configuration

Never guess task paths from %TEMP%, $HOME, or the current working directory.

Return results people can understand

A useful module result has three layers:

Summary: one sentence

Answer: What completed, and does it need attention?

Good:

Checked DNS registration and found one forward/reverse mismatch.

Weak:

Done.

Tests: one row per check

Put tests in data.tests:

{
  "name": "DNS forward lookup",
  "status": "passed",
  "summary": "The hostname resolved to 10.20.30.40.",
  "evidence": {
    "address_count": 1
  }
}

Use these statuses consistently:

  • passed: expected condition was confirmed;
  • warning: usable result with something worth reviewing;
  • failed: the check ran and confirmed a bad condition, or could not complete critically;
  • informational: context with no pass/fail judgment;
  • not_applicable: the check does not apply to this device;
  • unknown: evidence was insufficient for a conclusion.

Findings: explain important conditions

A finding should tell the technician what the condition means and what to do next:

{
  "severity": "warning",
  "title": "Forward and reverse DNS do not match",
  "description": "The selected hostname and returned address do not map back to each other.",
  "evidence": {
    "forward_count": 1,
    "reverse_match": false
  },
  "recommendation": "Validate the current DHCP lease and approved DNS registration workflow."
}

Do not put secrets, full authentication headers, raw cookies, or unnecessary personal data in evidence.

Add parameters without building UI code

The manifest creates parameter controls automatically.

"parameters": [
  {
    "name": "lookback_days",
    "label": "Lookback period",
    "description": "Collect files modified within this many days.",
    "type": "integer",
    "default": 7,
    "minimum": 1,
    "maximum": 30
  },
  {
    "name": "mode",
    "label": "Assessment mode",
    "type": "choice",
    "default": "standard",
    "choices": ["standard", "extended"]
  }
]

Available types are integer, string, string_list, boolean, and choice. Unknown parameters and invalid values are rejected before your script starts. Your script must still validate meaning—for example, an allowed URL scheme, hostname format, or safe filesystem boundary.

Use a picker instead of free text

For supported live resources, add resource_provider and resource_view to a string parameter. Current providers are:

  • installed_applications with list;
  • qualified_users with list;
  • active_directory_ous with tree.

Picker values can become stale. Resolve the selected object again immediately before an action and enforce authorization in the action itself. Modules with live-resource parameters cannot be saved in a pack.

PowerShell or Python?

Choose PowerShell when you need Windows services, registry, event logs, DISM, CIM/WMI, or WinRM execution on the target. Code for Windows PowerShell 5.1 unless the application contract changes.

Choose Python for API integrations, deterministic data analysis, and report generation that runs on the technician computer. Use only dependencies already packaged with the application or add them to the approved build deliberately.

Both SDKs provide context loading, progress, cancellation, and normalized results. The examples show the smallest complete pattern in each language.

Build a safe action

Actions deserve extra care because they change state.

In the manifest:

  • set module_type to action;
  • set a realistic risk level;
  • set requires_confirmation to true;
  • set volatile to true when the action can disconnect a user, interrupt work, or change quickly;
  • declare elevation and target-admin requirements truthfully.

In code, use this order:

  1. Preflight: verify target, permissions, paths, service state, and exact selected resource.
  2. No-change exit: if preflight fails, return a result stating that nothing changed.
  3. Cancellation check: stop before the first mutation when requested.
  4. Bounded mutation: change only the documented resource.
  5. Verification: query the final state independently.
  6. Recovery: use finally when interruption could leave a service stopped or a file half-moved.
  7. Result: list changes_made, partial-change state, restart requirements, and excluded operations.

The action example intentionally writes only to its approved task artifact directory, making it safe to learn from and run.

Make a log collector bounded

Use the PowerShell SDK’s Invoke-MuonBoundedLogCollection unless a product requires a specialized capture session. The helper handles:

  • lookback and total-size limits;
  • per-file limits and allowed extensions;
  • cancellation;
  • focused EVTX export;
  • hashing and ZIP creation;
  • local/remote attachment return;
  • skipped-source reporting.

Never collect browser profiles, credential stores, tokens, unrelated personal folders, or an unbounded directory tree. If a source is missing or access is denied, record that limitation instead of failing the entire archive silently.

Make remote support real

For current target-side WinRM execution, use a PowerShell entry point and declare:

"supports_remote": true,
"execution_scope": "target",
"allowed_transports": ["local", "winrm"]

A WinRM module must run with no desktop, prompt, mapped drive, or interactive user assumption. Remember:

  • HKCU, %USERPROFILE%, and %LOCALAPPDATA% belong to the remoting identity;
  • process and user-session visibility may differ from an interactive sign-in;
  • queries must originate inside the staged remote script when they claim target-side evidence;
  • context paths may be serialized path objects, so use SDK path helpers;
  • remote diagnostics and collectors should return a Remote collection scope test;
  • remote actions should return a Remote action scope test;
  • test with an authorized support identity and a denied identity.

Use controller scope only when the operation must originate on the technician workstation, such as an RSAT or API query:

"supports_remote": true,
"execution_scope": "controller",
"allowed_transports": ["local"]

Controller scope does not mean “pretend remote support works.” The code must still use the selected target deliberately.

Run the static/remoting audit before release:

.\scripts\audit_remote_modules.ps1

Then pilot through real domain WinRM. The audit cannot reproduce enterprise delegation, firewall, product permissions, or interactive-user visibility.


Complete manifest reference

module.json is validated against schemas/module.schema.json. Unknown properties are rejected, so spelling mistakes do not silently become unused settings.

Identity and presentation

Field Required Meaning
schema_version Yes Must currently be 1.0.
id Yes Permanent unique ID, such as network.dns-health. Use lowercase letters and digits separated by ., _, or -.
name Yes Technician-facing name, up to 100 characters.
version Yes Semantic version, such as 1.0.0 or 1.1.0-beta.1.
description Yes Short explanation used on tiles, confirmations, and generated help.
category Yes Category tab or grouping, such as Networking, Windows, or Hardware.
author No Owning person or team.
tags No Search terms that help technicians find the tool.
icon No Reserved module icon metadata. Do not depend on custom tile rendering unless the host supports it.
help_file No Same-folder Markdown filename, normally HELP.md.
minimum_app_version No Oldest toolkit version that understands the module contract. Repository publication enforces this.
supported_operating_systems No Human-readable supported OS list. The module must still perform any required runtime OS check.

The entry point must be a file directly inside the module folder. Nested supporting files are allowed, but entry_point and help_file cannot escape the folder.

Execution and placement

Field Required Meaning
language Yes powershell or python.
entry_point Yes main.ps1 or main.py-style filename.
module_type Yes The module’s behavior and current UI placement.
supports_local Yes The module works when This computer is selected.
supports_remote Yes The module correctly uses the selected remote target.
execution_scope No target by default, or controller when work originates on the technician computer.
allowed_transports Yes Execution paths the implementation actually supports.
timeout_seconds Yes Hard host timeout from 1 to 86,400 seconds.
supports_cancellation No Whether the module checks and honors cooperative cancellation. Defaults to true.
extension No true marks an optional, usually vendor-specific module. Extensions stay hidden from module lists and packs until a technician enables them on the Extensions page.
extension_group No Name shared by extensions that are enabled and disabled together, such as a vendor’s health check and its log collector ("CrowdStrike"). The page shows the group as one row. Requires extension: true.

Current automatic page placement is:

module_type Current application behavior
diagnostic, action, analyzer, integration Appears under Troubleshooting Modules.
log_collector Appears under Log Collection and receives a Log Files result tab.
report_generator, connection_test, dashboard_provider Valid host-extension contracts, but not automatically registered on those specialized pages yet.

For a normal target-side Windows module, use execution_scope: "target". Local runs execute on the technician computer when it is the selected target; remote PowerShell runs are staged and executed on the endpoint through WinRM. For controller-side tools—such as an RSAT query or an HTTPS integration—use execution_scope: "controller" and allowed_transports: ["local"]; the selected target remains in the context, but the code runs on the technician computer.

The built-in host currently executes only these remote contracts:

  • target-side PowerShell with winrm;
  • target-context API modules with api;
  • controller-origin modules with local.

Other transport names are reserved by the schema and must not be declared until a working provider is implemented and tested in the host.

Risk, confirmation, and authorization

Field Meaning
risk_level Impact level: informational, low, medium, high, or critical.
volatile The operation can change quickly, disrupt a session, or make later evidence differ.
requires_confirmation Requires an explicit technician confirmation before launch.
requires_elevation Documents that the operation needs an elevated process.
requires_local_admin Documents that local administrator rights are required.
authorization.allowed_groups Windows token groups allowed to run the module. An empty list imposes no allow-list.
authorization.denied_groups Windows token groups explicitly denied. Deny wins over allow.
authorization.require_any_allowed_group When true and an allow-list exists, membership in at least one listed group is required.
authorization.require_local_admin Enforces local administrator membership in the current authorization service.
authorization.require_elevation Declares elevation in the authorization contract. The module must still perform a target-side preflight.

Keep the top-level requirement flags and their matching authorization values consistent. A manifest check is not a substitute for a code-level preflight: remote permissions, product roles, file ACLs, and service control rights can differ from the technician workstation’s local token.

Use confirmation for every intentional mutation. High- and critical-risk modules, volatile modules, and actions included in a pack also receive host confirmation, but declare requires_confirmation: true rather than relying on incidental host behavior.

Output declaration

All current modules return normalized JSON:

"outputs": {
  "format": "json",
  "schema": "../../schemas/result.schema.json"
}

The relative schema path is documentation for developers and repository tooling. The running host validates the returned envelope against its installed result schema.


Complete execution-context reference

The context is host-owned input. Treat it as read-only. A representative context looks like this:

{
  "schema_version": "1.0",
  "task_id": "0d4c...",
  "module_id": "network.dns-health",
  "module_version": "1.0.0",
  "target": {
    "target_name": "PC001",
    "fqdn": "PC001.example.com",
    "ip_addresses": ["10.20.30.40"],
    "is_local": false,
    "is_online": true,
    "domain": "example.com",
    "connection_methods": {"winrm": true},
    "interactive_user": "EXAMPLE\\jsmith",
    "session": {}
  },
  "execution": {
    "transport": "winrm",
    "scope": "target",
    "timeout_seconds": 300,
    "working_directory": "...",
    "remote_port": 5985,
    "remote_use_ssl": false
  },
  "parameters": {},
  "integrations": {},
  "paths": {
    "output": "...\\result.json",
    "logs": "...\\module.log",
    "temporary": "...",
    "cancellation": "...\\cancel.requested",
    "sdk_powershell": "...\\MuonInsight.ModuleSdk.psm1",
    "artifacts": "..."
  }
}

Not every field is guaranteed to contain a value. For example, interactive_user, FQDN, IP addresses, and online state may be unknown. Read optional values defensively.

Path rules

  • Use paths.temporary for disposable working data.
  • Use paths.artifacts for files that must be returned to the technician.
  • Do not write paths.output yourself when using Complete-MuonModule or context.result().
  • Check paths.cancellation through the SDK rather than opening it.
  • In PowerShell, pass context paths through Resolve-MuonContextPath; remoting serialization can turn a path into an object containing a value property.
  • Resolve and compare paths before writing. A filename built from user input must not escape the approved root.
  • Do not assume the current directory, %TEMP%, %USERPROFILE%, or a mapped drive belongs to the intended user or target.

For remote WinRM runs, the host creates a temporary directory on the endpoint, stages the entire module folder plus the PowerShell SDK, executes the entry point non-interactively, retrieves declared attachments, and removes staging. Remote integration settings are deliberately omitted. A target-side module must not expect centrally configured secrets or controller-only paths in its remote context.


Toolkit interaction API

The supported way for a module to interact with the toolkit is the module SDK. It provides context loading, progress events, cancellation checks, result creation, and selected file/process helpers. It does not expose the Qt interface or allow a module to add controls at runtime.

PowerShell SDK: exported functions

Import the exact SDK path supplied by the host:

$preview = Get-Content -Raw -LiteralPath $ContextPath | ConvertFrom-Json
Import-Module ([string]$preview.paths.sdk_powershell) -Force
$context = Import-MuonContext -ContextPath $ContextPath

The module exports these public functions.

Import-MuonContext

Import-MuonContext -ContextPath <string>

Reads and deserializes the execution-context JSON. It throws when the file is missing. Use the returned object for all task, target, parameter, execution, integration, and path values.

Write-MuonProgress

Write-MuonProgress -Context $context -Progress 40 -Stage 'Collecting' -Message 'Reading services'

Writes a host protocol event that updates Running Tasks. Progress may be an integer or $null for indeterminate work. The host clamps numeric values to 0–100. Keep Stage short and stable; put the current human-readable operation in Message.

Progress is not a result and is not retained as diagnostic evidence. Do not put secrets or full command output in progress messages.

Test-MuonCancellation

if (Test-MuonCancellation -Context $context) {
    Complete-MuonModule $context cancelled 'The operation was cancelled safely.' `
        -Data @{ tests = $tests } -StartedAt $startedAt | Out-Null
    exit 2
}

Returns true after the technician requests cancellation. Check it before expensive work, between bounded batches, and immediately before the first mutation. Cancellation is cooperative; it does not roll back changes automatically.

Complete-MuonModule

Complete-MuonModule `
    -Context $context `
    -Status success `
    -Summary 'The assessment completed without a warning.' `
    -Data @{ analysis = 'Readable analysis'; tests = $tests } `
    -Findings $findings `
    -Attachments $attachments `
    -Errors $errors `
    -Metrics @{ checks = $tests.Count } `
    -StartedAt $startedAt | Out-Null

Creates the complete result envelope, writes it to paths.output, and emits the result protocol record to the host. Call it exactly once on every completion path, including caught failures and cancellation. PowerShell modules should emit one of success, warning, failed, cancelled, or partially_completed; timeout, force termination, unauthorized, pending, and running states are owned by the host.

New-MuonTest and New-MuonFinding

$tests = @()
$findings = @()

$script:tests += New-MuonTest 'DNS Client service' 'passed' 'The service is running.' @{ status = 'Running' }
$script:findings += New-MuonFinding 'warning' 'Resolution failed' 'The probe name did not resolve.' `
    @{ name = 'www.microsoft.com' } 'Check DNS server reachability.'

Build one test (name, status, summary, evidence) or one finding (severity, title, description, evidence, recommendation) in the shape results expect. Parameters are positional in that order.

They return the record and do not append it, so always write the +=. A call without it produces a record that goes nowhere. An SDK function cannot append for you: a function in the SDK .psm1 resolves $script: to the SDK’s own scope, so it would add to a list the module never sees, with no error. Write $script:tests rather than $tests so a call inside one of the module’s own functions still reaches the script-level list.

Resolve-MuonContextPath

$temporary = Resolve-MuonContextPath -Value $context.paths.temporary

Returns a usable string whether a path arrived as a string, dictionary, or remoted object with a value property. Use it for every path read from the context.

Test-MuonAdministrator

if (-not (Test-MuonAdministrator)) { ... }

Reports whether the token the module is running under holds the built-in Administrator role, and returns $false rather than throwing when the role cannot be read. Modules that change machine state use it to publish an honest privilege test instead of failing partway through. It answers for the running token only, so over WinRM it describes the connecting identity.

Get-MuonProperty

$lookback = Get-MuonProperty $context.parameters 'event_lookback_days' 7
$capacity = Get-MuonProperty $reportRow 'design_capacity'

Reads one optional property and returns -Default when the object is null, the property is absent, or its value is null. It handles both shapes a module actually holds: PSObjects from registry, CIM, certificate, and event sources, and hashtables the module built itself. A hashtable exposes Count and Keys through PSObject.Properties rather than its entries, so reading one with a PSObject-only accessor silently returns nothing.

A property that is present and $false is a value, not an absence, and is returned as $false. That distinction matters for registry policy values, where absent means platform default.

Get-MuonFileSha256

$digest = Get-MuonFileSha256 -Path $file.FullName

Returns a lowercase SHA-256 digest for a file. Use it when creating attachments or file evidence.

Invoke-MuonReadOnlyProcess

$run = Invoke-MuonReadOnlyProcess -Context $context `
    -FilePath "$env:SystemRoot\System32\wevtutil.exe" `
    -Arguments @('gli', 'System') -TimeoutSeconds 30

Runs a noninteractive child process with redirected output, cooperative cancellation, and a bounded timeout. It returns:

Property Meaning
started Whether the child process started.
exit_code Process exit code, or null after timeout/cancellation.
timed_out The helper stopped waiting because the timeout elapsed.
cancelled The helper observed toolkit cancellation.
duration_seconds Elapsed time.
output Combined, trimmed standard output and standard error.
error Exception type if process startup/handling failed.

The name expresses intended use; the helper cannot determine whether the executable or arguments are actually read-only. Do not use it to hide an undeclared mutation. Pass an executable path and argument array—never concatenate untrusted input into a shell command.

Invoke-MuonBoundedLogCollection

exit (Invoke-MuonBoundedLogCollection `
    -Context $context `
    -Label 'Example product' `
    -Patterns @('%ProgramData%\Vendor\Product\Logs\*.log') `
    -Channels @('System', 'Application') `
    -ApplicationEventFilter 'Provider[@Name="Vendor Provider"]' `
    -AdditionalTests $tests `
    -Findings $findings `
    -AdditionalData @{ product_version = $version } `
    -AnalysisSummary 'The product recorded two recent startup failures.')

Creates a complete log-collector result. It reads declared lookback_days and maximum_total_mb parameters, then:

  • expands the supplied file patterns on the execution target;
  • de-duplicates candidates;
  • allows .log, .txt, .etl, .evtx, .json, .xml, .csv, and .html;
  • enforces a 25 MB per-file limit and a 10–500 MB total limit;
  • includes only files inside the 1–30 day lookback;
  • exports bounded EVTX channels with wevtutil;
  • records copied and skipped sources;
  • hashes collected files;
  • writes a collection manifest;
  • creates a ZIP attachment; and
  • returns the normalized result.

The helper returns exit code 0 after normal completion and 2 after cooperative cancellation. It owns the final Complete-MuonModule call, so do not call that function again after invoking it.

Python SDK: ModuleContext

Import the SDK from the packaged application:

from muon_insight.module_sdk import ModuleContext

context = ModuleContext.from_argv()

Construction

API Meaning
ModuleContext.from_argv() Requires exactly one context-path argument and loads it. Normal entry points use this.
ModuleContext.load(path) Loads a specific context path. Useful for controlled tests.

Properties

Property Type Meaning
raw dict Complete context. Use defensively for fields without a convenience property.
started_at str Timestamp captured when the context object was created.
task_id str Unique run identifier.
module_id str Immutable module ID.
target_name str Selected target name.
parameters dict Copy of host-validated parameter values.
integrations dict Copy of approved non-secret integration configuration available to this execution scope.
temporary Path Resolved task workspace.
artifacts Path Resolved artifact directory; it is created automatically. Falls back to the temporary directory.

For optional target/execution data, use context.raw, for example:

target = dict(context.raw.get("target", {}))
is_local = bool(target.get("is_local", False))
transport = str(context.raw.get("execution", {}).get("transport", "local"))

context.cancelled()

Returns true when the host cancellation file exists. Check between expensive operations and before a mutation.

context.progress(percent, stage, message="", **counts)

context.progress(50, "Analyzing", "Evaluating records", processed=125, total=250)

Emits a progress protocol record. percent may be an integer or None. Extra integer counts are added to the event for future host or log consumers; the current task view primarily renders progress, stage, and message.

context.result(...)

context.result(
    "success",
    "The assessment completed.",
    data={"analysis": "No warning condition was detected.", "tests": tests},
    findings=findings,
    attachments=attachments,
    errors=errors,
    metrics={"records_evaluated": len(records)},
)

Creates, writes, and emits the normalized result. Call it exactly once on each completion path. The method returns the envelope dictionary, which is useful in unit tests.

The Python SDK intentionally does not provide Windows command, WinRM, bounded-log, UI, credential, or HTTP helpers. Use Python primarily for controller-side analysis and integrations, rely only on packaged dependencies, and implement strict time and size bounds around external I/O.

Protocol details for advanced debugging

The SDK writes compact JSON lines prefixed with MUON_PROGRESS: and MUON_RESULT:. These prefixes are a host protocol—not a general logging format. Normal modules should call the SDK instead of constructing the lines themselves. Other standard output becomes bounded technical details and is not a substitute for a result. A process exit code alone never creates a valid module result.

Modules cannot directly:

  • open or modify toolkit UI widgets;
  • add arbitrary buttons or dialogs;
  • read another task’s state;
  • store or retrieve the toolkit’s saved Windows credentials;
  • bypass target, parameter, authorization, or confirmation checks;
  • change their manifest after launch; or
  • assume that arbitrary Python packages or PowerShell modules exist.

Use manifest parameters for user input, progress for status, cancellation checks for cooperative stop, and a normalized result for all output.


Result contract in detail

The result is what the technician, reports, packs, and automation consume. A successful process with an invalid result is still a failed module run.

Top-level fields

Field Purpose
schema_version Currently 1.0.
task_id, module_id, module_version, target Copied from context by the SDK.
started_at, completed_at ISO timestamps.
status Overall outcome.
summary One concise technician-facing sentence.
findings Important interpreted conditions.
data Structured module-specific data; put diagnostic tests here.
attachments Files intentionally returned to the technician.
errors Safe, structured problems and limitations.
metrics Small numeric operational measurements.

Recommended module-emitted statuses are:

Status Use when
success The module completed and found no condition requiring attention.
warning The module completed but found a concern or meaningful limitation.
failed The declared operation could not complete or confirmed a terminal failure.
cancelled The module honored a cooperative cancellation request.
partially_completed An action or multi-stage operation made some progress but did not fully complete.

The host assigns timed_out, force_terminated, skipped, and unauthorized when appropriate.

Tests

Every readable check belongs in data.tests:

{
  "name": "Windows Update service",
  "status": "passed",
  "summary": "The service is present and not disabled.",
  "evidence": {
    "state": "Running",
    "start_mode": "Manual"
  }
}

The only test statuses are passed, warning, failed, informational, not_applicable, and unknown. Keep evidence bounded and machine-readable. Do not put JSON text inside a JSON string.

Findings

Use this stable shape:

{
  "severity": "warning",
  "title": "Windows Update service is disabled",
  "description": "The disabled service can prevent update scans and deployments.",
  "evidence": {"service": "wuauserv", "start_mode": "Disabled"},
  "recommendation": "Validate policy, then run the confirmed Windows Update repair if appropriate."
}

Use informational, warning, or critical consistently for technician interpretation. A test states what one check returned; a finding explains why an important condition matters. Healthy modules should still return useful tests. The application can generate a compact Findings overview from tests, but module-authored findings remain the authoritative guidance.

Errors and partial data

{
  "category": "event_log_unavailable",
  "message": "The System event log could not be read.",
  "suggested_next_step": "Confirm event-log permissions and retry from an authorized session."
}

Use stable, lowercase categories suitable for filtering. A noncritical source failure should normally produce a warning result with the remaining evidence retained, rather than discarding everything. Do not return raw authorization headers, command lines containing secrets, full tracebacks, or exception objects.

Attachments

Recommended attachment metadata is:

{
  "name": "product-logs.zip",
  "path": "C:\\approved\\artifacts\\product-logs.zip",
  "media_type": "application/zip",
  "size_bytes": 1048576,
  "sha256": "lowercase-hex-digest",
  "kind": "log_collection"
}

The file must exist and remain inside the approved artifact/task workspace. For remote modules, the host retrieves declared files and replaces the remote path with a local one. A log collector should use kind: "log_collection"; the host expands the ZIP safely and maps data.files entries into the Log Files tab.

Each data.files record should contain:

{
  "source": "C:\\ProgramData\\Vendor\\Product\\Logs\\client.log",
  "file": "001-client.log",
  "path": "C:\\approved\\workspace\\001-client.log",
  "size_bytes": 24576,
  "sha256": "lowercase-hex-digest"
}

Do not attach unbounded folders, user credential stores, browser profile databases, or files that were not disclosed by the collection scope.

Metrics

Metrics should be small, numeric, and operational—for example records_evaluated, files_collected, or bytes_collected. The host adds duration_ms when it is absent. Do not duplicate all evidence in metrics.


Parameters and toolkit-generated controls

The host validates and renders all declared parameters before starting the module.

Type UI/input Useful constraints
integer Number control minimum, maximum, default
string One-line text or resource picker minimum_length, maximum_length, placeholder, required
string_list One value per line maximum_items, default, required
boolean Checkbox Boolean default
choice Drop-down list choices, string default

Unknown parameters, wrong types, out-of-range integers, overlong strings/lists, and invalid choices are rejected before execution. Domain validation remains the module’s responsibility. Examples include URL scheme allow-lists, hostname syntax, GUID shape, OU boundaries, and ensuring a warning threshold is lower than a critical threshold.

Live resource pickers

A string parameter can request one of the host’s current resource providers:

{
  "name": "application_id",
  "label": "Installed application",
  "type": "string",
  "required": true,
  "resource_provider": "installed_applications",
  "resource_view": "list"
}

Available providers are:

Provider View Returned selection
installed_applications list A host-discovered installed-application identity. Protected products are filtered by enterprise policy.
qualified_users list A target-specific eligible user/account identity.
active_directory_ous tree An OU distinguished name inside the configured approved root. Discovery runs through controller RSAT.

The module receives only the selected value, not the resource metadata shown in the picker. Re-query and revalidate the resource immediately before a mutation. Because live selections can become stale, modules using resource providers must run individually and cannot be saved into troubleshooting packs.


Files, dependencies, and module-owned assets

A production folder may contain supporting files:

vendor-product-health/
|-- module.json
|-- main.ps1
|-- HELP.md
|-- rules.json
`-- lib/
    `-- Parser.psm1

Remote WinRM staging copies the complete folder recursively, so use $PSScriptRoot to locate adjacent assets. Keep the payload bounded and do not write back into the installed module folder. Python modules may import other source files from their own folder: the host puts the module folder on the import path for the run, as modules/log-collection-chrome does with its capture and analysis files. Give those files distinctive names, because a same-folder file named like a standard-library module is imported in its place. Third-party packages must be deliberately included in the application build; a package installed only on a developer workstation is not a production dependency.

Never invoke a sibling module directly. Shared, stable host interaction belongs in the SDK; reusable domain logic may live in a carefully versioned module-owned library.


Credentials and external integrations

Plaintext saved credentials are never placed in the execution context. Modules must not prompt for a password, accept secrets as manifest parameters, inspect the UI process, or read arbitrary Credential Manager entries.

Controller-side integration modules may receive approved non-secret settings in context.integrations, such as an endpoint URL, table name, timeout, or feature flag. Validate those settings before use and enforce HTTPS, timeouts, response-size limits, pagination limits, and field allow-lists. Follow the toolkit’s approved credential-integration pattern when a host-managed credential is required; do not invent a new secret storage convention inside a module.

Target-side WinRM contexts intentionally receive an empty integrations object. If endpoint collection needs centrally configured non-secret data, pass only an explicitly declared, validated parameter or extend the reviewed host contract. Do not copy a service credential to the remote endpoint.


Pack compatibility

A module can be included in a troubleshooting pack when it needs only ordinary manifest parameters. The pack stores parameter values and may declare dependencies on other modules. It does not pass one module’s arbitrary result directly into another module.

Pack behavior to design for:

  • the pack is remote-compatible only if every included module supports remote execution;
  • dependencies must be explicit in the pack definition;
  • a failed prerequisite skips dependent modules;
  • optional missing modules do not block the whole pack;
  • action/volatile/high-risk content triggers pack confirmation;
  • each module keeps its own authorization, timeout, parameter validation, and result; and
  • live resource-picker modules cannot be included.

Do not make correctness depend on execution order unless the pack declares that dependency.


Minimal complete entry-point patterns

PowerShell diagnostic skeleton

[CmdletBinding()]
param([Parameter(Mandatory = $true)][string]$ContextPath)

Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$startedAt = Get-Date
$preview = Get-Content -Raw -LiteralPath $ContextPath | ConvertFrom-Json
Import-Module ([string]$preview.paths.sdk_powershell) -Force
$context = Import-MuonContext -ContextPath $ContextPath
$tests = @()
$findings = @()
$errors = @()

try {
    Write-MuonProgress $context 10 'Preparing' 'Validating inputs'
    $threshold = [int]$context.parameters.threshold

    if (Test-MuonCancellation $context) {
        Complete-MuonModule $context cancelled 'Cancelled before collection.' `
            -Data @{ tests = $tests } -StartedAt $startedAt | Out-Null
        exit 2
    }

    Write-MuonProgress $context 50 'Collecting' 'Reading target evidence'
    # Replace with bounded, read-only module logic.
    $observed = 0
    $tests += [ordered]@{
        name = 'Example check'
        status = if ($observed -gt $threshold) { 'warning' } else { 'passed' }
        summary = "Observed $observed against threshold $threshold."
        evidence = @{ observed = $observed; threshold = $threshold }
    }

    $status = if (@($tests | Where-Object status -eq 'warning').Count) { 'warning' } else { 'success' }
    Write-MuonProgress $context 100 'Complete' 'Assessment completed'
    Complete-MuonModule $context $status 'The example assessment completed.' `
        -Data @{ analysis = 'One bounded check was evaluated.'; tests = $tests } `
        -Findings $findings -Errors $errors `
        -Metrics @{ checks = $tests.Count } -StartedAt $startedAt | Out-Null
    exit 0
}
catch {
    $errors += [ordered]@{
        category = 'collection_failed'
        message = $_.Exception.Message
        suggested_next_step = 'Confirm access to the required data source and retry.'
    }
    Complete-MuonModule $context failed 'The example assessment could not complete.' `
        -Data @{ tests = $tests } -Findings $findings -Errors $errors `
        -StartedAt $startedAt | Out-Null
    exit 1
}

Python analyzer/integration skeleton

from __future__ import annotations

from typing import Any

from muon_insight.module_sdk import ModuleContext


def main() -> int:
    context = ModuleContext.from_argv()
    tests: list[dict[str, Any]] = []
    findings: list[dict[str, Any]] = []

    try:
        context.progress(10, "Preparing", "Validating inputs")
        threshold = int(context.parameters["threshold"])

        if context.cancelled():
            context.result("cancelled", "Cancelled before analysis.", data={"tests": tests})
            return 2

        context.progress(50, "Analyzing", "Evaluating bounded evidence")
        observed = 0  # Replace with deterministic, bounded logic.
        warning = observed > threshold
        tests.append(
            {
                "name": "Example check",
                "status": "warning" if warning else "passed",
                "summary": f"Observed {observed} against threshold {threshold}.",
                "evidence": {"observed": observed, "threshold": threshold},
            }
        )

        context.progress(100, "Complete", "Analysis completed")
        context.result(
            "warning" if warning else "success",
            "The example analysis completed.",
            data={"analysis": "One bounded check was evaluated.", "tests": tests},
            findings=findings,
            metrics={"checks": len(tests)},
        )
        return 0
    except (KeyError, TypeError, ValueError) as exc:
        context.result(
            "failed",
            "The example analysis could not complete.",
            data={"tests": tests},
            findings=findings,
            errors=[
                {
                    "category": "analysis_failed",
                    "message": str(exc),
                    "suggested_next_step": "Validate inputs and retry.",
                }
            ],
        )
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

Use the fully commented folders under repository-examples/modules for production starting points; these short skeletons show the contract but intentionally omit module-specific safety logic.

Handle failures without losing the result

Wrap the main operation in try/catch or try/except. A caught failure should still produce a valid result with:

  • a clear failed or partial summary;
  • completed tests retained;
  • a safe error category and message;
  • a suggested next step when known;
  • no stack trace or secret in technician-facing evidence.

Use raw technical details for debugging only. Never make technicians decode a Python exception or PowerShell property error to understand whether a check ran.

Test before publishing

Minimum development checklist

  • ☐ The manifest validates and the module appears in the expected page/category.
  • ☐ Local execution returns a valid result.
  • ☐ Every check appears in Test Results.
  • ☐ Important conditions appear in Findings with recommendations.
  • ☐ Cancellation works between expensive steps.
  • ☐ Failure paths still return a normalized result.
  • ☐ Parameters reject invalid input before mutation.
  • ☐ Help search finds the module by name, ID, product, and symptom.
  • ☐ No secrets or unnecessary personal data appear in any tab or attachment.
  • ☐ Remote execution was tested if supports_remote is true.
  • ☐ Actions verify the final state and describe partial changes.

Repository checks

python -m pytest
.\scripts\audit_remote_modules.ps1

Also run ruff and mypy for Python changes and parse every PowerShell file under Windows PowerShell 5.1. The release build repeats the automated suite and packaged health check.

Publish and update safely

  • Keep the module ID unchanged for the life of the tool.
  • Increment the semantic version when behavior changes.
  • Update HELP.md whenever parameters, evidence, permissions, target support, or risks change.
  • Hash the exact published bytes.
  • Apply the organization’s Authenticode and repository-signing policy.
  • Publish through the trusted module repository; do not ask users to run loose scripts from email.

Where to go next

  • Module example library: what each complete example demonstrates.
  • Help article authoring: a copyable technician-help template.
  • Architecture: host internals after you understand the module contract.
  • Configuration reference: enterprise settings and locks.
  • Credential and Active Directory model: approved identity and secret boundaries.

When in doubt, start by copying the diagnostic example and make the smallest safe change that produces one readable test result.

Something wrong or unclear? Open an issue on GitHub.