Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
dfa19a3
Prototype: codegen-owned static Change Safety parameters (alternative…
YangAn-microsoft Jul 16, 2026
f5b1c78
Prototype: exclude data-plane cmdlets from Change Safety static param…
YangAn-microsoft Jul 16, 2026
fa81926
Align Change Safety static param help text with SDK (ChangeSafetyPara…
YangAn-microsoft Jul 17, 2026
ac04dce
Make Change Safety static parameters opt-in via change-safety config …
YangAn-microsoft Jul 17, 2026
754eb05
Change Safety: verb-based write-cmdlet gating + rename config to enab…
YangAn-microsoft Jul 23, 2026
80acbef
Change Safety: emit inline IDynamicParameters instead of static param…
YangAn-microsoft Jul 29, 2026
e19eea4
Change Safety: emit PolicyTokenHandler as a separate delegate after O…
YangAn-microsoft Jul 31, 2026
e0e016e
Change Safety: always emit PolicyTokenHandler plumbing; gate only the…
YangAn-microsoft Jul 31, 2026
87c5059
Restrict policy token handler wiring to control plane
YangAn-microsoft Aug 3, 2026
4ce7bb5
Rename VTable slot PolicyTokenHandler to AddChangeSafetyPolicyTokenHa…
YangAn-microsoft Aug 3, 2026
d30ce48
Change Safety: forward dynamic params through the exported proxy
YangAn-microsoft Aug 3, 2026
0ef7785
Gate Change Safety plumbing on enable-change-safety; refresh emitter …
YangAn-microsoft Aug 3, 2026
50fe8d4
Merge remote-tracking branch 'upstream/main' into feature/change-safe…
YangAn-microsoft Aug 10, 2026
8ca5de9
Change Safety: fold psm1 policy-token handler into control-plane bloc…
YangAn-microsoft Aug 10, 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
55 changes: 54 additions & 1 deletion powershell/cmdlets/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Switch, System, TerminalCase, toExpression, Try, Using, valueOf, Field, IsNull, Or, ExpressionOrLiteral, TerminalDefaultCase, xmlize, TypeDeclaration, And, IsNotNull, PartialMethod, Case, While, LiteralStatement, Not, ElseIf
} from '@azure-tools/codegen-csharp';
import { ClientRuntime, EventListener, Schema, ArrayOf, EnumImplementation } from '../llcsharp/exports';
import { NullableBoolean, Alias, ArgumentCompleterAttribute, PSArgumentCompleterAttribute, AsyncCommandRuntime, AsyncJob, CmdletAttribute, ErrorCategory, ErrorRecord, Events, InvocationInfo, OutputTypeAttribute, ParameterAttribute, PSCmdlet, PSCredential, SwitchParameter, ValidateNotNull, verbEnum, GeneratedAttribute, DescriptionAttribute, ExternalDocsAttribute, CategoryAttribute, ParameterCategory, ProfileAttribute, PSObject, InternalExportAttribute, ExportAsAttribute, DefaultRunspace, RunspaceFactory, AllowEmptyCollectionAttribute, DoNotExportAttribute, HttpPathAttribute, NotSuggestDefaultParameterSetAttribute } from '../internal/powershell-declarations';
import { NullableBoolean, Alias, ArgumentCompleterAttribute, PSArgumentCompleterAttribute, AsyncCommandRuntime, AsyncJob, CmdletAttribute, ErrorCategory, ErrorRecord, Events, IDynamicParameters, InvocationInfo, OutputTypeAttribute, ParameterAttribute, PSCmdlet, PSCredential, SwitchParameter, ValidateNotNull, verbEnum, GeneratedAttribute, DescriptionAttribute, ExternalDocsAttribute, CategoryAttribute, ParameterCategory, ProfileAttribute, PSObject, InternalExportAttribute, ExportAsAttribute, DefaultRunspace, RunspaceFactory, AllowEmptyCollectionAttribute, DoNotExportAttribute, HttpPathAttribute, NotSuggestDefaultParameterSetAttribute } from '../internal/powershell-declarations';
import { State } from '../internal/state';
import { Channel } from '@autorest/extension-base';
import { IParameter } from '@azure-tools/codemodel-v3/dist/code-model/components';
Expand Down Expand Up @@ -460,6 +460,9 @@ export class CmdletClass extends Class {
// implement part of the IContext
this.implementIContext();

// Change Safety: emit codegen-owned inline dynamic parameters on write-verb cmdlets
this.implementChangeSafetyParameters();

// add constructors
this.implementConstructors();

Expand Down Expand Up @@ -1622,6 +1625,56 @@ export class CmdletClass extends Class {
extensibleParametersProp.get = toExpression(`${extensibleParameters.value} `);
this.add(extensibleParametersProp);
}
private implementChangeSafetyParameters() {
// Change Safety (codegen-owned inline dynamic parameters): only azure management-plane (ARM)
// write cmdlets expose the opt-in Change Safety parameters. They are surfaced via an inline
// IDynamicParameters implementation (the same dynamic-parameter mechanism SDK cmdlets use),
// emitted directly in the generated cmdlet -- no runtime-wired VTable delegate. PowerShell
// binds the returned parameters into BoundParameters, where the module-level HTTP pipeline
// step (in Az.Accounts) reads them.
// Opt-in per module via `enable-change-safety: true` in the module readme (default off), so
// the 180+ module rollout is deliberate rather than triggered by any unrelated regeneration.
if (!this.state.project.enableChangeSafety) {
return;
}
if (!this.state.project.azure) {
return;
}
// Change Safety is a management-plane concept; skip data-plane modules, which target a
// custom service endpoint (identified by an endpoint-resource-id-key-name). This matches
// the data-plane detection used in module-class.ts (isDataPlane).
if (this.state.project.endpointResourceIdKeyName) {
return;
}
// Gate on the PowerShell verb rather than sniffing the HTTP method off requests[0]. A cmdlet's
// callGraph can contain multiple operations (e.g. a GetPut Update = GET + PUT) and a single
// operation can carry multiple requests, so requests[0] is not a reliable signal for whether
// the cmdlet mutates. The verb encodes the mutating intent computed across the whole call graph
// and, unlike the raw HTTP method, correctly treats POST "list"-style operations (surfaced as
// Get) as reads.
const verb = (this.operation.details.csharp.verb ?? '').toLowerCase();
const readOnlyVerbs = ['get', 'test', 'find', 'search', 'measure', 'show', 'ping', 'trace', 'resolve', 'read'];
if (readOnlyVerbs.includes(verb)) {
return;
}

// Emit an inline IDynamicParameters implementation. GetDynamicParameters builds the
// RuntimeDefinedParameterDictionary locally with names + help text that mirror
// azure-powershell-common's ChangeSafetyParameters (there is no compile-time link across repos;
// a contract pin test in Accounts.Test keeps these literals in sync). This matches how SDK
// cmdlets surface -AcquirePolicyToken / -ChangeReference, so the two are indistinguishable.
this.interfaces.push(IDynamicParameters);
const getDynamicParameters = this.add(new Method('GetDynamicParameters', dotnet.Object, {
description: 'Returns the Change Safety dynamic parameters (-AcquirePolicyToken / -ChangeReference) for this write cmdlet.',
returnsDescription: 'A <see cref="System.Management.Automation.RuntimeDefinedParameterDictionary" /> containing the Change Safety parameters.'
}));
getDynamicParameters.add(function* () {
yield 'var parameters = new global::System.Management.Automation.RuntimeDefinedParameterDictionary();';
yield 'parameters.Add("AcquirePolicyToken", new global::System.Management.Automation.RuntimeDefinedParameter("AcquirePolicyToken", typeof(global::System.Management.Automation.SwitchParameter), new global::System.Collections.ObjectModel.Collection<global::System.Attribute> { new global::System.Management.Automation.ParameterAttribute { HelpMessage = "Acquire an Azure Policy token automatically for this resource operation.", ParameterSetName = global::System.Management.Automation.ParameterAttribute.AllParameterSets } }));';
yield 'parameters.Add("ChangeReference", new global::System.Management.Automation.RuntimeDefinedParameter("ChangeReference", typeof(string), new global::System.Collections.ObjectModel.Collection<global::System.Attribute> { new global::System.Management.Automation.ParameterAttribute { HelpMessage = "The change reference resource ID for this resource operation.", ParameterSetName = global::System.Management.Automation.ParameterAttribute.AllParameterSets } }));';
yield 'return parameters;';
});
}
private implementIEventListener() {
const $this = this;
const cts = this.add(new Field('_cancellationTokenSource', System.Threading.CancellationTokenSource, {
Expand Down
5 changes: 5 additions & 0 deletions powershell/generators/psm1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export async function generatePsm1(project: Project) {
let requestHandler = `
# Tweaks the pipeline per call
$instance.OnNewRequest = $VTable.OnNewRequest`;
if (project.enableChangeSafety) {
requestHandler += `
# Tweaks the pipeline per call (Change Safety policy-token step)
$instance.AddChangeSafetyPolicyTokenHandler = $VTable.AddChangeSafetyPolicyTokenHandler`;
}
if (project.endpointResourceIdKeyName) {
// for data plane, we should append different functions instead.
requestHandler = `
Expand Down
1 change: 1 addition & 0 deletions powershell/internal/powershell-declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const ErrorRecord: TypeDeclaration = new ClassType(sma, 'ErrorRecord');
export const SwitchParameter: TypeDeclaration = new ClassType(sma, 'SwitchParameter');
export const NullableBoolean: TypeDeclaration = new ClassType(new Namespace('System'), 'Boolean?');
export const IArgumentCompleter: IInterface = { allProperties: [], declaration: 'System.Management.Automation.IArgumentCompleter' };
export const IDynamicParameters: IInterface = { allProperties: [], declaration: 'System.Management.Automation.IDynamicParameters' };
export const CompletionResult: TypeDeclaration = new ClassType(sma, 'CompletionResult');
export const CommandAst: TypeDeclaration = new ClassType(`${sma}.Language`, 'CommandAst');
export const CompletionResultType: TypeDeclaration = new ClassType(sma, 'CompletionResultType');
Expand Down
6 changes: 6 additions & 0 deletions powershell/internal/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export class Project extends codeDomProject {
public modelCmdletFolder!: string;
public endpointResourceIdKeyName!: string;
public endpointSuffixKeyName!: string;
public enableChangeSafety!: boolean;

public customFolder!: string;
public utilsFolder!: string;
Expand Down Expand Up @@ -334,6 +335,11 @@ export class Project extends codeDomProject {
// configuration for whether to use fixed array in generated code of model, default is false
this.fixedArray = await this.state.getValue('fixed-array', false);

// Change Safety: opt-in per module. When true, azure management-plane write-verb cmdlets get the
// -AcquirePolicyToken / -ChangeReference parameters. Default false so the rollout is controlled
// (a module opts in, alongside bumping its Az.Accounts minimum), not organic on every regeneration.
this.enableChangeSafety = await this.state.getValue('enable-change-safety', false);

// File paths
this.csproj = await this.state.getValue('csproj');
this.dll = await this.state.getValue('dll');
Expand Down
15 changes: 15 additions & 0 deletions powershell/module/module-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ export class NewModuleClass extends Class {
pipelineChangeDelegate.fullDefinition, /* prependStep */
pipelineChangeDelegate.fullDefinition)); /* appendStep */

// Change Safety: dedicated append-only delegate for the policy-token pipeline step.
const changeSafetyPolicyTokenDelegate = new Alias('ChangeSafetyPolicyTokenDelegate', System.Action(
InvocationInfo, /* invocationInfo */
pipelineChangeDelegate.fullDefinition)); /* appendStep */

const argumentCompleterDelegate = namespace.add(new Alias('ArgumentCompleterDelegate', System.Func(
dotnet.String, /* completerName */
InvocationInfo, /* invocationInfo */
Expand Down Expand Up @@ -305,6 +310,9 @@ export class NewModuleClass extends Class {
}

namespace.add(newRequestPipelineDelegate);
if (!isDataPlane && this.state.project.enableChangeSafety) {
namespace.add(changeSafetyPolicyTokenDelegate);
}

const incomingSignalDelegate = namespace.add(new Alias('SignalDelegate', this.incomingSignalFunc));
const eventListenerDelegate = namespace.add(new Alias('EventListenerDelegate', this.eventListenerFunc));
Expand All @@ -314,6 +322,7 @@ export class NewModuleClass extends Class {
/* AzAccounts VTable properties */
const OnModuleLoad = this.add(new Property('OnModuleLoad', moduleLoadPipelineDelegate, { description: 'The delegate to call when this module is loaded (supporting a commmon module).' }));
const OnNewRequest = new Property('OnNewRequest', newRequestPipelineDelegate, { description: 'The delegate to call before each new request (supporting a commmon module).' });
const AddChangeSafetyPolicyTokenHandler = new Property('AddChangeSafetyPolicyTokenHandler', changeSafetyPolicyTokenDelegate, { description: 'Change Safety: delegate called after OnNewRequest to (conditionally) add the policy-token pipeline step.' });
const AddRequestUserAgentHandler = new Property('AddRequestUserAgentHandler', newRequestPipelineDelegate, { description: 'The delegate to call before each new request to add request user agent.' });
const AddPatchRequestUriHandler = new Property('AddPatchRequestUriHandler', newRequestPipelineDelegate, { description: 'The delegate to call before each new request to patch request uri.' });
const AddAuthorizeRequestHandler = new Property('AddAuthorizeRequestHandler', authorizeRequestDelegate, { description: 'The delegate to call before each new request to add authorization.' });
Expand All @@ -329,6 +338,9 @@ export class NewModuleClass extends Class {
this.add(AddAuthorizeRequestHandler);
} else {
this.add(OnNewRequest);
if (this.state.project.enableChangeSafety) {
this.add(AddChangeSafetyPolicyTokenHandler);
}
}
const GetParameterValue = this.add(new Property('GetParameterValue', getParameterDelegate, { description: 'The delegate to call to get parameter data from a common module.' }));
const EventListener = this.add(new Property('EventListener', eventListenerDelegate, { description: 'A delegate that gets called for each signalled event' }));
Expand Down Expand Up @@ -402,6 +414,9 @@ export class NewModuleClass extends Class {
yield `${AddAuthorizeRequestHandler.value}?.Invoke( ${$this.pInvocationInfo.use}, ${fendpointResourceIdKeyName},${fEndpointSuffixKeyName}, (step)=> { ${pip}.Prepend(step); } , (step)=> { ${pip}.Append(step); }, ${fTokenAudienceConverter}, ${$this.pExtensibleParameters} );`;
} else {
yield `${OnNewRequest.value}?.Invoke( ${$this.pInvocationInfo.use}, ${$this.pCorrelationId},${$this.pProcessRecordId}, (step)=> { ${pip}.Prepend(step); } , (step)=> { ${pip}.Append(step); } );`;
if ($this.state.project.enableChangeSafety) {
yield `${AddChangeSafetyPolicyTokenHandler.value}?.Invoke( ${$this.pInvocationInfo.use}, (step)=> { ${pip}.Append(step); } );`;
}
}
yield Return(pip);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ protected override void ProcessRecord()
}
sb.Append($"){Environment.NewLine}{Environment.NewLine}");

sb.Append(variantGroup.ToDynamicParamOutput());
sb.Append(variantGroup.ToBeginOutput());
sb.Append(variantGroup.ToProcessOutput());
sb.Append(variantGroup.ToEndOutput());
Expand Down
55 changes: 55 additions & 0 deletions powershell/resources/psruntime/BuildTime/Models/PsProxyOutputs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,59 @@ public string ClearTelemetryContext()
}
}

internal class DynamicParamOutput : BaseOutput
{
public DynamicParamOutput(VariantGroup variantGroup) : base(variantGroup)
{
}

// Change Safety: only emit a dynamicparam block when a wrapped (private) cmdlet actually declares
// dynamic parameters via IDynamicParameters. For every other cmdlet this emits nothing (zero diff),
// so it is a no-op that just forwards the private cmdlet's runtime parameters through the proxy.
private bool HasDynamicParameters() => VariantGroup.Variants.Any(v =>
{
var implementingType = (v.Info as CmdletInfo)?.ImplementingType;
return implementingType != null && typeof(IDynamicParameters).IsAssignableFrom(implementingType);
});

private string GetParameterSetToCmdletMapping()
{
var sb = new StringBuilder();
sb.AppendLine($"{Indent}$mapping = @{{");
foreach (var variant in VariantGroup.Variants)
{
sb.AppendLine($@"{Indent}{Indent}{variant.VariantName} = '{variant.PrivateModuleName}\{variant.PrivateCmdletName}';");
}
sb.Append($"{Indent}}}");
return sb.ToString();
}

public override string ToString() => !HasDynamicParameters() ? String.Empty : $@"dynamicparam {{
{Indent}$parameterSet = $PSCmdlet.ParameterSetName
{GetParameterSetToCmdletMapping()}
{Indent}if (-not $mapping.ContainsKey($parameterSet)) {{ $parameterSet = @($mapping.Keys)[0] }}
{Indent}try {{
{Indent}{Indent}$targetCmd = $ExecutionContext.InvokeCommand.GetCommand(($mapping[$parameterSet]), [System.Management.Automation.CommandTypes]::Cmdlet, $PSBoundParameters)
{Indent}{Indent}$dynamicParams = @($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object {{ $_.Value.IsDynamic }})
{Indent}{Indent}if ($dynamicParams.Length -gt 0) {{
{Indent}{Indent}{Indent}$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
{Indent}{Indent}{Indent}foreach ($param in $dynamicParams) {{
{Indent}{Indent}{Indent}{Indent}$param = $param.Value
{Indent}{Indent}{Indent}{Indent}if (-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name)) {{
{Indent}{Indent}{Indent}{Indent}{Indent}$dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
{Indent}{Indent}{Indent}{Indent}{Indent}$paramDictionary.Add($param.Name, $dynParam)
{Indent}{Indent}{Indent}{Indent}}}
{Indent}{Indent}{Indent}}}
{Indent}{Indent}{Indent}return $paramDictionary
{Indent}{Indent}}}
{Indent}}} catch {{
{Indent}{Indent}throw
{Indent}}}
}}

";
}

internal class BeginOutput : BaseOutput
{
public BeginOutput(VariantGroup variantGroup) : base(variantGroup)
Expand Down Expand Up @@ -640,6 +693,8 @@ public static string ToSyntaxTypeName(this Type type)

public static ParameterNameOutput ToParameterNameOutput(this string parameterName, bool isLast) => new ParameterNameOutput(parameterName, isLast);

public static DynamicParamOutput ToDynamicParamOutput(this VariantGroup variantGroup) => new DynamicParamOutput(variantGroup);

public static BeginOutput ToBeginOutput(this VariantGroup variantGroup) => new BeginOutput(variantGroup);

public static ProcessOutput ToProcessOutput(this VariantGroup variantGroup) => new ProcessOutput(variantGroup);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ protected override void ProcessRecord()
}
sb.Append($"){Environment.NewLine}{Environment.NewLine}");

sb.Append(variantGroup.ToDynamicParamOutput());
sb.Append(variantGroup.ToBeginOutput());
sb.Append(variantGroup.ToProcessOutput());
sb.Append(variantGroup.ToEndOutput());
Expand Down
Loading
Loading