RFC #12 (2/5): derive PSRP & host-call messages#21
Merged
irvingouj@Devolutions (irvingoujAtDevolution) merged 19 commits intoJun 23, 2026
Conversation
…en FromParams/ToPs bodies Migrate the 8 host param/return types (Coordinates, Size, Rectangle, BufferCell, KeyInfo, ProgressRecord, FieldDescription, ChoiceDescription, PSCredential) to #[derive(PsSerialize, PsDeserialize)] using the now crate-agnostic macro. Dual camelCase+PascalCase emission via #[ps(also=..)], <TN> chains via #[ps(type_names(..))], SecureString credential blob and the nested ProgressRecordType enum via #[ps(with=..)], lenient host params via #[ps(default)]. The ~700 lines of hand-written PsValue construction/parsing in host/params.rs and host/returns.rs collapse to: - the derived conversions (zero manual PsValue logic), plus - thin positional adapters (arg(a,i) / Some(v.to_ps_value())) — argument plumbing, not CLIXML conversion. The only remaining hand-built object is the genuinely dynamic Prompt-result Hashtable (runtime field names). Byte-for-byte safety: added host::snapshot_test characterization tests that lock the exact CLIXML of the return types (incl. the credential SecureString and dual-casing) captured from the old impls, and assert the derived path reproduces them identically. Full workspace build, 40 test groups, clippy and fmt all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
New field attr nil_when_none: an Option<..> field is always emitted (as Nil when None) instead of being omitted — required by objects whose slot must be present. Migrate CommandParameter (N is Nil when positional, V is the dynamic escape-hatch value) to the derive. create_pipeline roundtrip tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…ssages Define RemoteHostMethodId (MS-PSRP §2.2.3.17, 56 variants) as a PsEnum object — the mi enum-object is now macro-derived (PsEnums<I32> is byte-identical to the old ExtendedPrimitive<I32>, ToString = variant name). Migrate RUNSPACEPOOL_HOST_CALL and PIPELINE_HOST_CALL to #[derive]: ci (i64), mi (RemoteHostMethodId), mp (Vec<PsValue>, the method-specific params stay the dynamic boundary). Replaces method_id:i32 + method_name:String + hand-built mi. client-core's define_host_methods! now matches on the typed phc.method (exhaustive over the enum) instead of an i32 literal; an unknown method id is rejected when the wire enum is parsed (test repurposed accordingly). The real-capture parse_real_pipeline_host_call test passes (WriteProgress + its ProgressRecord param now go through the derived path). Workspace green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
Migrate RUNSPACEPOOL_HOST_RESPONSE and PIPELINE_HOST_RESPONSE to #[derive]: ci (i64), mi (RemoteHostMethodId), mr/me (optional dynamic-value boundary). All four host call/response messages now build their mi enum-object by macro; no hand-written ComplexObject in any of them. Threaded the typed method end-to-end across crates: - HostCall::method() -> RemoteHostMethodId (generated by define_host_methods!); - RemoteHostMethodId::from_id(i32) for the transport result path; - client-core send_*_host_response and the CancelHostCall op now carry RemoteHostMethodId instead of (method_id: i32, method_name: String); - web/tokio response construction uses host_call.method(). Workspace builds, 40 test groups, clippy and fmt green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…ipeline New field attr to_string: also set the object's <ToString> from a String field. Migrate Command and PowerShellPipeline to #[derive]: - Command: cmd (#[ps(to_string)] -> the object's ToString, matching pwsh), Args = Vec<CommandParameter> (plain ArrayList; the old to_string on Args was a divergence from captured pwsh XML and is dropped), UseLocalScope (nil_when_none), 8 merge fields via a lenient merge_result_conv (#[ps(with)]) that tolerates flag combinations like Output|Error=3 -> None, matching the old behavior. - PowerShellPipeline: cmds = Vec<Command>, History via a with-converter (empty -> Nil). The exact-XML round-trip command tests pass unchanged, proving byte fidelity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
ProgressRecordType -> PsEnum object repr; ProgressRecord -> #[derive], keeping its TypedBuilder transforms (parent-id negative filter, percent clamp). The old negative-filter/-1-default on the parse side were dead defensive paths (the builder normalizes before serialize); the roundtrip tests pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…e ApplicationPrivateData Add reusable ToPsValue/FromPsValue for BTreeMap<String, PsValue> (a PSPrimitiveDictionary <DCT>). Migrate APPLICATION_PRIVATE_DATA to #[derive]: data: Option<BTreeMap<..>> with #[ps(nil_when_none)] (Nil when absent). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…licationArguments/PSVersionTable New PsObject struct mode `#[ps(dictionary)]`: serialize fields as a <DCT> PSPrimitiveDictionary keyed by field name (instead of an <MS> property bag), with `#[ps(flatten)]` merging a BTreeMap field's entries into the same dict (and collecting unclaimed keys on parse). Migrate PSVersionTable and ApplicationArguments to the derive (dictionary mode + Version/version-array with-converters + flatten for additional_arguments). The hand-built dictionary ComplexObject construction is gone; round-trip tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…HostInfo/HostDefaultData
Eliminates the last hand-written ComplexObject in the host_info cluster.
HostDefaultData now derives an integer-keyed {T,V} value dictionary via
#[ps(value_dictionary, key, type_tag)]; HostInfo nests it one level under a
'data' member via #[ps(wrap)]. ValueWrapper and all manual TryFrom/From impls
are gone. Wire bytes verified unchanged by create_pipeline exact-XML test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
Both now derive their CLIXML conversions; the only bespoke bit is app_args_conv collapsing empty ApplicationArguments to Nil (a primitive, not a built object). Wire bytes verified by the create_pipeline exact-XML characterization test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…ject/to_string-with ErrorRecord + ErrorCategory now fully macro-derived. New reusable macro features: #[ps(flatten_prefix)] (prefix-flatten a nested Option<Struct>), #[ps(fallback_object)] (read a field from a nested sibling object when absent up top — for .NET records that nest payload in Exception), and #[ps(to_string)] now works for any Display field. Real-world CommandNotFound XML verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…hic message_data Adds the PsUnion derive (untagged polymorphic enum: primitive/type_match/ fallback dispatch) so InformationMessageData's 3-way union converts to/from PsValue by macro. HostInformationMessage + InformationRecord now fully derive. TimeGenerated (DateTime|String) and the index-keyed Tags String[] use small typed converters (no object literals). ZERO hand-built ComplexObject literals remain in the messages layer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…equire dispatch) Review hardening: #[ps(value_dictionary)] fields now honor a #[ps(with)] converter instead of silently ignoring it; PsUnion now rejects (at compile time) any variant lacking exactly one dispatch mode, which previously would serialize but never deserialize. No behavior change for current consumers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
A PowerShell <Nil/> member is semantically equivalent to an absent one. For Option/#[ps(default)] fields, a present-but-Nil property must map to None/Default instead of being fed to from_ps_value (which rejects Nil). Fixes WriteProgress rejection when ProgressRecord.CurrentOperation arrives as <Nil/>.
…ed auth Over plain HTTP, SSPI message sealing is used (application/HTTP-SPNEGO-session- encrypted). Over HTTPS, TLS provides confidentiality so the body is sent plain (application/soap+xml) and authentication is connection-oriented (RFC 4559): the connection is authenticated once during the handshake (the first operation rides the SPNEGO challenge legs) and every subsequent operation reuses that authenticated connection. requires_sspi_sealing() is therefore true only for plain HTTP. Key points: - SSPI INTEGRITY (sign) is requested unconditionally; CONFIDENTIALITY (seal) only when sealing. INTEGRITY over HTTPS lets the server trust the connection and produces the NTLM MIC / SPNEGO mechListMIC. - EPA channel binding (tls-server-end-point): learned from the server cert after a 401 and applied on a restarted auth sequence. - AlreadyComplete is handled consistently across all four transports (tokio direct, gateway, sync, web). - Fail fast on terminal auth rejection (401 -> PwshCoreError::Auth), try_join! so a failed handshake short-circuits, and the non-interactive client exits non-zero on failure / clean on success (command_completed is authoritative). - Basic over plain HTTP is refused unless --http-insecure is given.
…lding Add the 10-case transport_auth_matrix asserting the WinRM rule against a real server: HTTP+SSPI seals; HTTPS+SSPI is unsealed (TLS); Basic refused over plain HTTP unless forced, allowed (unsealed) over HTTPS. Helpers: sealed() detects the multipart/encrypted envelope, connected() detects pipeline completion. Also makes the shared e2e scaffolding auth-aware: e2e_pwsh_config gains default_auth_method(); native_pty_matrix and pty_harness use it (the default DC refuses Basic over HTTP), and fall back to the standard e2e host/creds out of the box.
test (5/5): transport × auth × sealing e2e matrix
feat (4/5): spec-correct WinRM HTTP/HTTPS transport + connection-oriented auth
fix (3/5): treat present-but-Nil property as absent in PsDeserialize
12ba49b
into
stack/01-rfc12-derive-engine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 2 of 5 — base:
stack/01-rfc12-derive-engine(stacked on #20).Applies the derive engine to the PSRP message set and host calls: typed
PsrpMessagestream (L4), tagged property-bag collapse,#[ps(dictionary/flatten/ value_dictionary/wrap/to_string/nil_when_none)], typedRemoteHostMethodId, andderives for session/capability/runspace/progress/host-info/error/information records.
Second half of the RFC #12 series. Review after #20.