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:
- Pick the example closest to what you want.
- Copy its entire folder.
- Rename the module in
module.json. - Replace the small, clearly marked example logic.
- 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:
- Load the supplied context.
- Read only declared parameters and approved integration settings.
- Report useful progress for operations that take time.
- Check cancellation between expensive steps.
- Collect bounded evidence or perform the exact confirmed change.
- 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_applicationswithlist;qualified_userswithlist;active_directory_ouswithtree.
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_typetoaction; - set a realistic risk level;
- set
requires_confirmationtotrue; - set
volatiletotruewhen the action can disconnect a user, interrupt work, or change quickly; - declare elevation and target-admin requirements truthfully.
In code, use this order:
- Preflight: verify target, permissions, paths, service state, and exact selected resource.
- No-change exit: if preflight fails, return a result stating that nothing changed.
- Cancellation check: stop before the first mutation when requested.
- Bounded mutation: change only the documented resource.
- Verification: query the final state independently.
- Recovery: use
finallywhen interruption could leave a service stopped or a file half-moved. - 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.temporaryfor disposable working data. - Use
paths.artifactsfor files that must be returned to the technician. - Do not write
paths.outputyourself when usingComplete-MuonModuleorcontext.result(). - Check
paths.cancellationthrough the SDK rather than opening it. - In PowerShell, pass context paths through
Resolve-MuonContextPath; remoting serialization can turn a path into an object containing avalueproperty. - 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_remoteis 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.mdwhenever 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.