Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a6d3f12
Add support for `Actions`
SteveL-MSFT Sep 8, 2026
7f929a3
Implement action discovery
SteveL-MSFT Sep 11, 2026
315cc5a
implement support for actions in configure get
SteveL-MSFT Sep 16, 2026
08074e1
integrate actions into engine
SteveL-MSFT Sep 19, 2026
db3297e
fix loc and clippy
SteveL-MSFT Sep 25, 2026
bcc7ad2
Refactor DSC action descriptions and input validation
SteveL-MSFT Sep 28, 2026
3c08323
address copilot feedback
SteveL-MSFT Sep 28, 2026
7539cb9
refactor code to abstract actions and adapters as resources
SteveL-MSFT Oct 22, 2026
16f42a4
Fix loc issues
SteveL-MSFT Oct 22, 2026
f955404
fix regression in detecting supporting filtering
SteveL-MSFT Oct 23, 2026
deccccc
Fix issue where adapter wasn't being found
SteveL-MSFT Oct 23, 2026
131e08c
fix tests
SteveL-MSFT Oct 24, 2026
f818df7
Address PR feedback: action error handling, schema validation, locale…
SteveL-MSFT Oct 24, 2026
6e96fd6
Don't write action input to stdin when passed via JSON arg
SteveL-MSFT Oct 24, 2026
cbc9d91
Allow discover extensions to return action manifests; scan action man…
SteveL-MSFT Oct 24, 2026
4908805
Reuse already-loaded action schemas in list_dsc_actions
SteveL-MSFT Oct 24, 2026
8847466
Add ActionManifest and DscAction imports
SteveL-MSFT Oct 24, 2026
6124476
fix mishandling of path by making it an option
SteveL-MSFT Oct 24, 2026
8824110
Address review: inline action directories, manifest lists, adapter re…
SteveL-MSFT Oct 24, 2026
047be17
make action manifest directory option
SteveL-MSFT Oct 24, 2026
c0742c1
fix dscaction path allowed ot be Option
SteveL-MSFT Oct 25, 2026
ac3a58b
fix so dscresource path/directory is option
SteveL-MSFT Oct 25, 2026
fa7bed2
address copilot feedback
SteveL-MSFT Oct 25, 2026
c52670e
address copilot feedback
SteveL-MSFT Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# CFS/ADO crate feed is currently only working with unstable features.
[unstable]
registry-auth = true

[registries]
DSCCargoMirror = { index = "sparse+https://pkgs.dev.azure.com/msazure/One/_packaging/DSCCargoMirror/Cargo/index/" }

Expand Down
9 changes: 2 additions & 7 deletions dsc-bicep-ext/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@

use clap::Parser;
use dsc_lib::{
configure::config_doc::ExecutionKind,
discovery::discovery_trait::DiscoveryFilter,
dscresources::{
dscresource::Invoke,
invoke_result::{GetResult, SetResult},
},
DscManager, configure::config_doc::ExecutionKind, discovery::{discovery_trait::DiscoveryFilter},
resources::invoke_result::{GetResult, SetResult},
types::{FullyQualifiedTypeName, ResourceVersionReq},
DscManager,
};
use rust_i18n::{i18n, t};
use std::{env, io, process};
Expand Down
21 changes: 20 additions & 1 deletion dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ configAbout = "Apply a configuration document"
parameters = "Parameters to pass to the configuration as JSON or YAML"
parametersFile = "Parameters to pass to the configuration as a JSON or YAML file"
systemRoot = "Specify the operating system root path if not targeting the current running OS"
actionAbout = "Operations on DSC actions"
extensionAbout = "Operations on DSC extensions"
resourceAbout = "Invoke a specific DSC resource"
schemaAbout = "Get the JSON schema for a DSC type"
Expand Down Expand Up @@ -81,7 +82,7 @@ noParameters = "No parameters specified"

[resource_command]
implementedAs = "implemented as"
invalidOperationOnAdapter = "Can not perform this operation on the adapter itself"
invalidOperation = "Can not perform this operation on %{resource}"
setInputEmpty = "Desired input is empty"
testInputEmpty = "Expected input is required"
jsonError = "JSON: %{err}"
Expand Down Expand Up @@ -115,6 +116,7 @@ functionInvocationFailed = "Function '%{function}' invocation failed: %{error}"
[server.invoke_dsc_resource]
resourceNotFound = "Resource type '%{resource}' does not exist"
whatIfNotSupported = "what_if is only supported for the 'set' and 'delete' operations"
actionNotSupported = "The type '%{resource}' is an action or adapter and cannot be invoked as a resource"

[server.list_dsc_functions]
invalidNamePattern = "Invalid function name pattern '%{pattern}'"
Expand All @@ -124,8 +126,18 @@ invalidDescriptionPattern = "Invalid function description pattern '%{pattern}'"
resourceNotAdapter = "The resource '%{adapter}' is not a valid adapter"
adapterNotFound = "Adapter '%{adapter}' does not exist"

[server.invoke_dsc_action]
actionNotFound = "Action type '%{resource}' does not exist"
resourceNotSupported = "The type '%{resource}' is not an action"

[server.show_dsc_action]
actionNotFound = "Action type '%{type_name}' does not exist"
resourceNotSupported = "The type '%{type_name}' is not an action"

[server.show_dsc_resource]
resourceNotFound = "Resource type '%{type_name}' does not exist"
resourceManifestNotFound = "Resource manifest for '%{type_name}' not found"
Comment thread
SteveL-MSFT marked this conversation as resolved.
resourceNotSupported = "The type '%{type_name}' is an action or adapter and cannot be shown as a resource"

[subcommand]
actualStateNotObject = "actual_state is not an object"
Expand All @@ -148,6 +160,10 @@ noResources = "Resources not specified"
resourceTypeNotSpecified = "Resource type not specified"
validatingResource = "Validating resource named"
resourceNotFound = "Resource type not found"
tableHeader_action_operations = "Operations"
tableHeader_action_hasInput = "Input"
tableHeader_action_hasOutput = "Output"
tableHeader_action_requireSecurityContext = "Security"
tableHeader_type = "Type"
tableHeader_kind = "Kind"
tableHeader_version = "Version"
Expand All @@ -162,6 +178,9 @@ invalidFunctionDescriptionFilter = "Invalid function description filter"
maxInt = "maxInt"
invalidManifest = "Error in manifest for"
jsonArrayNotSupported = "JSON array output format is only supported for `--all'"
yes = "Yes"
no = "No"
none = "None"

[util]
failedToConvertJsonToString = "Failed to convert JSON to string"
Expand Down
17 changes: 17 additions & 0 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ pub enum SubCommand {
#[clap(long, hide = true)]
as_include: bool,
},
#[clap(name = "action", about = t!("args.actionAbout").to_string())]
Action {
#[clap(subcommand)]
subcommand: ActionSubCommand,
},
#[clap(name = "extension", about = t!("args.extensionAbout").to_string())]
Extension {
#[clap(subcommand)]
Expand Down Expand Up @@ -178,6 +183,18 @@ pub enum ConfigSubCommand {
}
}

#[derive(Debug, PartialEq, Eq, Subcommand)]
pub enum ActionSubCommand {
#[clap(name = "list", about = t!("args.listActionAbout").to_string())]
List {
/// Optional action name to filter the list
#[clap(default_value_t)]
action_name: TypeNameFilter,
#[clap(short = 'o', long, help = t!("args.outputFormat").to_string())]
output_format: Option<ListOutputFormat>,
},
}

#[derive(Debug, PartialEq, Eq, Subcommand)]
pub enum ExtensionSubCommand {
#[clap(name = "list", about = t!("args.listExtensionAbout").to_string())]
Expand Down
3 changes: 3 additions & 0 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ fn dsc_main() -> Result<(), ExitCode> {

subcommand::config(&subcommand, &merged_parameters, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format)?;
},
SubCommand::Action { subcommand } => {
subcommand::action(&subcommand, progress_format)?;
},
SubCommand::Extension { subcommand } => {
subcommand::extension(&subcommand, progress_format)?;
},
Expand Down
52 changes: 23 additions & 29 deletions dsc/src/resource_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,27 @@ use crate::args::{GetOutputFormat, OutputFormat};
use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, EXIT_DSC_RESOURCE_NOT_FOUND, write_object};
use dsc_lib::configure::config_doc::{Configuration, ExecutionKind};
use dsc_lib::configure::add_resource_export_results_to_configuration;
use dsc_lib::discovery::ResourceKind;
use dsc_lib::discovery::discovery_trait::DiscoveryFilter;
use dsc_lib::dscresources::{resource_manifest::Kind, invoke_result::{DeleteResultKind, GetResult, ResourceGetResponse, ResourceSetResponse, SetResult}};
use dsc_lib::dscresources::dscresource::{Capability, get_diff};
use dsc_lib::DscManager;
use dsc_lib::dscresources::dscresource::get_diff;
use dsc_lib::dscerror::DscError;
use dsc_lib::resources::{invoke_result::{DeleteResultKind, GetResult, ResourceGetResponse, SetResult, ResourceSetResponse}, resource_invoke::{Capability, ResourceInvoke}};
use dsc_lib::types::{FullyQualifiedTypeName, ResourceVersionReq};
use rust_i18n::t;
use serde_json::Value;
use std::process::ExitCode;
use tracing::{debug, error, info};

use dsc_lib::{
dscresources::dscresource::{Invoke, DscResource},
DscManager
};

pub fn get(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, version: Option<&ResourceVersionReq>, input: &str, format: Option<&GetOutputFormat>) -> Result<(), ExitCode> {
let Some(resource) = get_resource(dsc, resource_type, version) else {
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.map_or(String::new(), |v| v.to_string())));
return Err(ExitCode::from(EXIT_DSC_RESOURCE_NOT_FOUND));
};

debug!("{} {} {:?}", resource.type_name, t!("resource_command.implementedAs"), resource.implemented_as);
if resource.kind == Kind::Adapter {
error!("{}: {}", t!("resource_command.invalidOperationOnAdapter"), resource.type_name);
if resource.kind() == ResourceKind::Action || resource.kind() == ResourceKind::Adapter {
error!("{}", t!("resource_command.invalidOperation", resource = resource.type_name()));
return Err(ExitCode::from(EXIT_DSC_ERROR));
}

Expand Down Expand Up @@ -78,9 +75,8 @@ pub fn get_all(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, ver
return Err(ExitCode::from(EXIT_DSC_RESOURCE_NOT_FOUND));
};

debug!("{} {} {:?}", resource.type_name, t!("resource_command.implementedAs"), resource.implemented_as);
if resource.kind == Kind::Adapter {
error!("{}: {}", t!("resource_command.invalidOperationOnAdapter"), resource.type_name);
if resource.kind() == ResourceKind::Action || resource.kind() == ResourceKind::Adapter {
error!("{}", t!("resource_command.invalidOperation", resource = resource.type_name()));
return Err(ExitCode::from(EXIT_DSC_ERROR));
}

Expand Down Expand Up @@ -141,9 +137,8 @@ pub fn set(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, version
return Err(ExitCode::from(EXIT_DSC_RESOURCE_NOT_FOUND));
};

debug!("{} {} {:?}", resource.type_name, t!("resource_command.implementedAs"), resource.implemented_as);
if resource.kind == Kind::Adapter {
error!("{}: {}", t!("resource_command.invalidOperationOnAdapter"), resource.type_name);
if resource.kind() == ResourceKind::Action || resource.kind() == ResourceKind::Adapter {
error!("{}", t!("resource_command.invalidOperation", resource = resource.type_name()));
return Err(ExitCode::from(EXIT_DSC_ERROR));
}

Expand All @@ -160,7 +155,7 @@ pub fn set(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, version
Err(_) => true,
};

if !exist && resource.capabilities.contains(&Capability::Delete) && !resource.capabilities.contains(&Capability::SetHandlesExist) {
if !exist && resource.capabilities().contains(&Capability::Delete) && !resource.capabilities().contains(&Capability::SetHandlesExist) {
debug!("{}", t!("resource_command.routingToDelete"));

let before_state = match resource.get(input) {
Expand Down Expand Up @@ -236,9 +231,8 @@ pub fn test(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, versio
return Err(ExitCode::from(EXIT_DSC_RESOURCE_NOT_FOUND));
};

debug!("{} {} {:?}", resource.type_name, t!("resource_command.implementedAs"), resource.implemented_as);
if resource.kind == Kind::Adapter {
error!("{}: {}", t!("resource_command.invalidOperationOnAdapter"), resource.type_name);
if resource.kind() == ResourceKind::Action || resource.kind() == ResourceKind::Adapter {
error!("{}", t!("resource_command.invalidOperation", resource = resource.type_name()));
return Err(ExitCode::from(EXIT_DSC_ERROR));
}

Expand Down Expand Up @@ -268,9 +262,8 @@ pub fn delete(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, vers
return Err(ExitCode::from(EXIT_DSC_RESOURCE_NOT_FOUND));
};

debug!("{} {} {:?}", resource.type_name, t!("resource_command.implementedAs"), resource.implemented_as);
if resource.kind == Kind::Adapter {
error!("{}: {}", t!("resource_command.invalidOperationOnAdapter"), resource.type_name);
if resource.kind() == ResourceKind::Action || resource.kind() == ResourceKind::Adapter {
error!("{}", t!("resource_command.invalidOperation", resource = resource.type_name()));
return Err(ExitCode::from(EXIT_DSC_ERROR));
}

Expand All @@ -291,7 +284,7 @@ pub fn delete(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, vers
}
},
DeleteResultKind::SyntheticWhatIf(_) => {
info!("{} {}", resource.type_name, t!("resource_command.syntheticWhatIf"));
info!("{} {}", resource.type_name(), t!("resource_command.syntheticWhatIf"));
}
}
},
Expand All @@ -308,8 +301,9 @@ pub fn schema(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, vers
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.map_or(String::new(), |v| v.to_string())));
return Err(ExitCode::from(EXIT_DSC_RESOURCE_NOT_FOUND));
};
if resource.kind == Kind::Adapter {
error!("{}: {}", t!("resource_command.invalidOperationOnAdapter"), resource.type_name);

if resource.kind() == ResourceKind::Action || resource.kind() == ResourceKind::Adapter {
error!("{}", t!("resource_command.invalidOperation", resource = resource.type_name()));
return Err(ExitCode::from(EXIT_DSC_ERROR));
}

Expand Down Expand Up @@ -339,13 +333,13 @@ pub fn export(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, vers
return Err(ExitCode::from(EXIT_DSC_RESOURCE_NOT_FOUND));
};

if dsc_resource.kind == Kind::Adapter {
error!("{}: {}", t!("resource_command.invalidOperationOnAdapter"), dsc_resource.type_name);
if dsc_resource.kind() == ResourceKind::Action || dsc_resource.kind() == ResourceKind::Adapter {
error!("{}", t!("resource_command.invalidOperation", resource = dsc_resource.type_name()));
return Err(ExitCode::from(EXIT_DSC_ERROR));
}

let mut conf = Configuration::new();
if let Err(err) = add_resource_export_results_to_configuration(dsc_resource, &mut conf, input) {
if let Err(err) = add_resource_export_results_to_configuration(&*dsc_resource, &mut conf, input) {
error!("{err}");
return Err(ExitCode::from(EXIT_DSC_ERROR));
}
Expand All @@ -362,7 +356,7 @@ pub fn export(dsc: &mut DscManager, resource_type: &FullyQualifiedTypeName, vers
}

#[must_use]
pub fn get_resource<'a>(dsc: &'a mut DscManager, resource: &FullyQualifiedTypeName, version: Option<&ResourceVersionReq>) -> Option<&'a DscResource> {
pub fn get_resource(dsc: & mut DscManager, resource: &FullyQualifiedTypeName, version: Option<&ResourceVersionReq>) -> Option<Box<dyn ResourceInvoke>> {
//TODO: add dynamically generated resource to dsc
dsc.find_resource(&DiscoveryFilter::new(resource, version.cloned(), None)).unwrap_or(None)
}
73 changes: 73 additions & 0 deletions dsc/src/server/invoke_dsc_action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::server::mcp_server::McpServer;
use dsc_lib::{
DscManager, configure::config_doc::ExecutionKind,
discovery::{
discovery_trait::DiscoveryFilter,
ResourceKind,
},
types::FullyQualifiedTypeName
};
use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters};
use rust_i18n::t;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::task;

#[derive(Serialize, JsonSchema)]
pub struct InvokeDscActionResponse {
pub result: Value,
}

#[derive(Deserialize, JsonSchema)]
pub struct InvokeDscActionRequest {
#[schemars(description = "The type name of the DSC action to invoke")]
pub r#type: FullyQualifiedTypeName,
#[schemars(description = "The properties to pass to the DSC action as JSON. Must match the input schema from `show_dsc_action` tool.")]
pub properties_json: Option<String>,
#[schemars(description = "When true, request what-if (dry-run) execution. Only valid for actions that declare what-if support; otherwise the invocation fails.")]
#[serde(default)]
pub what_if: Option<bool>,
}

#[tool_router(router = invoke_dsc_action_router, vis = "pub")]
impl McpServer {
#[tool(
description = "Invoke a DSC action with specified properties in JSON format. Set 'what_if' to true to request what-if execution when supported.",
annotations(
title = "Invoke a DSC action with specified properties and optional what-if execution",
read_only_hint = false,
destructive_hint = true,
idempotent_hint = false,
open_world_hint = true,
)
)]
pub async fn invoke_dsc_action(&self, Parameters(InvokeDscActionRequest { r#type: type_name, properties_json, what_if }): Parameters<InvokeDscActionRequest>) -> Result<Json<InvokeDscActionResponse>, McpError> {
let result = task::spawn_blocking(move || {
let execution_kind = match what_if {
Some(true) => ExecutionKind::WhatIf,
_ => ExecutionKind::Actual,
};
let mut dsc = DscManager::new();
let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&type_name, None, None)).unwrap_or(None) else {
return Err(McpError::invalid_request(t!("server.invoke_dsc_action.actionNotFound", resource = type_name), None));
};
if resource.kind() != ResourceKind::Action {
return Err(McpError::invalid_request(t!("server.invoke_dsc_action.resourceNotSupported", resource = type_name), None));
}
let input = match properties_json {
Some(json) => json,
None => "".to_string(),
};
match resource.invoke(&input, &execution_kind) {
Ok(result) => Ok(result),
Err(e) => Err(McpError::internal_error(e.to_string(), None)),
}
}).await.map_err(|e| McpError::internal_error(e.to_string(), None))??;

Ok(Json(InvokeDscActionResponse { result }))
}
}
20 changes: 13 additions & 7 deletions dsc/src/server/invoke_dsc_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
use crate::server::mcp_server::McpServer;
use dsc_lib::{
DscManager, configure::config_doc::ExecutionKind,
discovery::discovery_trait::DiscoveryFilter,
dscresources::{
dscresource::Invoke,
discovery::{
discovery_trait::DiscoveryFilter,
ResourceKind,
},
resources::{
invoke_result::{
DeleteResult,
DeleteResultKind,
Expand Down Expand Up @@ -54,7 +56,8 @@ pub struct InvokeDscResourceRequest {
#[schemars(description = "The operation to perform on the DSC resource")]
pub operation: DscOperation,
#[schemars(description = "The type name of the DSC resource to invoke")]
pub resource_type: FullyQualifiedTypeName,
#[serde(alias="resource_type")]
pub r#type: FullyQualifiedTypeName,
#[schemars(description = "The properties to pass to the DSC resource as JSON. Must match the resource JSON schema from `show_dsc_resource` tool.")]
pub properties_json: String,
#[schemars(description = "When true and operation is 'set' or 'delete', simulate the change (what-if / dry-run) instead of applying it. Resources without native what-if support return a synthetic result derived from 'test'. Only valid with the 'set' and 'delete' operations.")]
Expand All @@ -74,7 +77,7 @@ impl McpServer {
open_world_hint = true,
)
)]
pub async fn invoke_dsc_resource(&self, Parameters(InvokeDscResourceRequest { operation, resource_type, properties_json, what_if }): Parameters<InvokeDscResourceRequest>) -> Result<Json<InvokeDscResourceResponse>, McpError> {
pub async fn invoke_dsc_resource(&self, Parameters(InvokeDscResourceRequest { operation, r#type, properties_json, what_if }): Parameters<InvokeDscResourceRequest>) -> Result<Json<InvokeDscResourceResponse>, McpError> {
let result = task::spawn_blocking(move || {
let execution_kind = if what_if.unwrap_or(false) {
if !matches!(operation, DscOperation::Set | DscOperation::Delete) {
Expand All @@ -85,9 +88,12 @@ impl McpServer {
ExecutionKind::Actual
};
let mut dsc = DscManager::new();
let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, None, None)).unwrap_or(None) else {
return Err(McpError::invalid_request(t!("server.invoke_dsc_resource.resourceNotFound", resource = resource_type), None));
let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&r#type, None, None)).unwrap_or(None) else {
return Err(McpError::invalid_request(t!("server.invoke_dsc_resource.resourceNotFound", resource = r#type), None));
};
if matches!(resource.kind(), ResourceKind::Action | ResourceKind::Adapter) {
return Err(McpError::invalid_request(t!("server.invoke_dsc_resource.actionNotSupported", resource = r#type), None));
}
match operation {
DscOperation::Get => {
let result = match resource.get(&properties_json) {
Expand Down
Loading
Loading