Skip to content

RFC #12 (1/5): PsValue layer + Ps derive macro engine#20

Merged
irvingouj@Devolutions (irvingoujAtDevolution) merged 33 commits into
masterfrom
stack/01-rfc12-derive-engine
Jun 23, 2026
Merged

RFC #12 (1/5): PsValue layer + Ps derive macro engine#20
irvingouj@Devolutions (irvingoujAtDevolution) merged 33 commits into
masterfrom
stack/01-rfc12-derive-engine

Conversation

@irvingoujAtDevolution

Copy link
Copy Markdown
Collaborator

Stack 1 of 5 — base: `master`.

Foundation of the RFC #12 CLIXML serialization refactor: the typed PsValue
access layer (L0/L1) and the `Ps` derive macro engine (`PsSerialize`/`PsDeserialize`/
`PsEnum`, `#[ps(with=)]`, nestable derives, crate-agnostic codegen).

This is the first half of the RFC #12 commit series (13 commits); the message-level
migrations that consume this engine are in the next PR in the stack.

Subsequent PRs stack on top:
2. `stack/02-rfc12-message-derives` — derive PSRP & host-call messages
3. `stack/03-writeprogress-nil` — fix Nil-as-absent in PsDeserialize
4. `stack/04-winrm-transport-auth` — spec-correct HTTP/HTTPS transport
5. `stack/05-transport-auth-matrix` — e2e matrix tests

Claude (claude) and others added 27 commits June 16, 2026 22:16
Introduce the ergonomic access layer the RFC prioritizes as the biggest
win per line — additive, no breaking changes to the value model.

L0 (known_types.rs): static primitive tag <-> .NET type const table and a
single-source GUID-uppercase convention helper (now used by From<Uuid>).

L1:
- convert.rs: FromPsValue / ToPsValue traits with impls for the primitive
  Rust types, Uuid, byte vectors, Vec<T> (lists) and Option<T> (Nil).
- builder.rs: ComplexObject::req/opt typed accessors (searching both
  adapted and extended bags, with precise missing/type-mismatch errors)
  and a fluent ComplexObjectBuilder that writes each property name once,
  hiding the PsProperty duplication and the two-bag representation.

Fix the as_string_array stub that silently returned Some(vec![]); it now
reads <LST>/<STK>/<QUE> containers and rejects non-string elements.

Migrate ConnectRunspacePool to the new API as the first exemplar
(~70 lines of hand-rolled trio -> ~25), preserving its roundtrip tests.

Add value_layer_tests covering accessors, builder, conversions and the
as_string_array fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…L3)

The "serde for CLIXML" derive layer: a message becomes an annotated struct
instead of a hand-written PsObjectWithType + From<T> for ComplexObject +
TryFrom<ComplexObject> trio.

- ironposh-macros: PsSerialize emits PsObjectWithType + From<T> for
  ComplexObject by building through the L1 ComplexObjectBuilder; PsDeserialize
  emits TryFrom<ComplexObject> via the req/opt accessors. Field attribute
  #[ps(name = "..")] sets the CLIXML property name; #[ps(adapted)] targets the
  adapted bag; Option<T> fields are omitted on serialize and tolerate
  absent/Nil on deserialize. Struct attribute #[ps(message_type = Variant)]
  binds the MessageType.
- Serialization goes by reference (&self.field), so no per-message clone.
- Add a blanket ToPsValue impl for &T to support borrow-based field emission.
- Migrate ConnectRunspacePool and PublicKey to the derive; their existing
  roundtrip/shape tests are unchanged and still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…erive

Convert two more property-bag messages to #[derive(PsSerialize,
PsDeserialize)]. Existing roundtrip/parse tests for RunspacePoolInitData are
unchanged and still pass. SessionCapability is intentionally left
hand-written: it uses the Version primitive and a String-stored-as-bytes
TimeZone, the kind of "gnarly type" the RFC keeps as a manual impl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…tach

Addresses the CONNECT_RUNSPACEPOOL min/max gap (issue #12, step 6). The
reattach path previously always built the pool with the creator default of
1/1, so a reattach to a differently-sized pool would advertise wrong limits.

Add Connector::new_connect_with_runspaces(config, shell_id, min, max) and an
optional connect_runspaces override threaded into the reattach
RunspacePoolCreator. Default behavior is unchanged (1/1) and documented
inline with a reference to the issue; proper fix is server-side session
enumeration, which we still do not implement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
Add the L4 typed protocol stream: PsrpMessage is the typed view of a wire
PowerShellRemotingMessage, so parsing happens once at the boundary instead
of every consumer re-running "match MessageType -> parse_ps_message ->
try_from". PsrpMessage::parse covers the receive-path message types and
preserves unmodeled/free-form payloads (debug/verbose/warning records,
unknown types) verbatim rather than failing, so the variant set can grow
incrementally. Large record variants are boxed to keep the enum compact.
Unit-tested for SessionCapability, RunspacePoolInitData and PipelineOutput.

Adopt it end-to-end in the WSMan Connect negotiation loop
(expect_shell_connected): the reattach ConnectResponse handler now matches
typed PsrpMessage variants instead of re-deriving and re-parsing per arm,
with identical behavior (same tolerance for unexpected messages). Covered
by the existing connect_mode_reaches_connected / reattach tests.

Note: the pool's main receive loop is intentionally left on its current
dispatch — its InformationRecord arm deliberately tolerates a strict parse
failure with a lossy fallback, which the strict typed stream would turn
into a hard error; that migration needs live-server validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…step 4)

The RFC's one breaking change: replace ComplexObject's two
`BTreeMap<String, PsProperty>` fields (adapted_properties + extended_properties)
with a single ordered, name-keyed `Properties` map whose entries are tagged
`PropertyKind::{Adapted, Extended}`. `PsProperty` is deleted — it duplicated the
map key it was stored under; a `Property { kind, value }` holds only the value.

Why now: the L1 accessors/builder (req/opt, ComplexObject::standard()) already
hide the representation for callers, which is exactly the precondition the RFC
set for doing this swap.

Wire compatibility is preserved byte-for-byte: serialization still emits
adapted (<Props>) before extended (<MS>), each sorted by name, and the
exact-XML + roundtrip test suites pass unchanged. Property reads now search
both member sets (RFC L1 finding #4: clients don't care about the
adapted/extended distinction) — this corrects one error_record test whose
expected string had encoded a doubled-comma artifact from the old
extended-only lookup.

Scope: ComplexObject + Properties/Property/PropertyKind, the serialize/
deserialize codec, the L1 builder/accessors, all ~19 message types, host
params/returns, the pool's in-place SecureString walk, and the web JS-interop
layer (JsComplexObject keeps its two JS-facing maps, converted at the
boundary). Full workspace build, tests (40 groups), clippy and fmt are green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…bility messages

Completes the L3 derive feature set from RFC #12: the derive now supports the
`#[ps(with = "module")]` custom-converter attribute (module provides
`to_ps_value(&T) -> PsValue` and `from_ps_value(&PsValue) -> Result<T, _>`),
which the RFC listed for "gnarly" fields. Works for required and optional
fields, extended and adapted bags.

Also add an identity `FromPsValue for PsValue` (the dynamic escape hatch) so
fields kept as raw PsValue can be derived.

Migrate three more messages off hand-written impls:
- RunspacePoolStateMessage / PipelineStateMessage: implement ToPsValue +
  FromPsValue for their I32-backed enums, then derive (enum field + raw
  Option<PsValue> escape-hatch field).
- SessionCapability: derive with `#[ps(with = ..)]` converters for its
  `<Version>` fields and the byte-array TimeZone blob — exercises the new
  attribute end to end.

Wire output unchanged; the exact-XML/roundtrip suites pass. 7 messages now use
the derive; the remaining ones are the genuinely nested/dictionary/multi-
location types the RFC explicitly keeps hand-written on the L1 API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…igrate Coordinates/Size

Enhance #[derive(PsSerialize, PsDeserialize)] so sub-objects compose:
- generate ToPsValue/FromPsValue for every derived type, so a field of a
  derived type (de)serializes as a nested <Obj> automatically;
- message_type is now optional — the same derive serves top-level messages and
  nested sub-objects;
- new struct attr #[ps(type_names("A","B"))] sets the <TN> chain;
- new field attrs #[ps(alias = "...")] (try multiple wire names) and
  #[ps(default)] (missing -> Default) for tolerant/multi-spelling fields.

Migrate Coordinates/Size (host_default_data) to the derive as the first nested
proof — ~50 lines of hand-written conversions removed, wire bytes unchanged
(exact-XML/roundtrip suites pass). Add derive_features tests covering nesting,
type_names, alias, default and the ToPsValue/FromPsValue bridge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
A review against the RFC found three additions exercised only by their own
tests, with no production consumer — speculative generality. Cut them:

- L0 PRIMITIVE_TYPES table + dotnet_type_for_tag: nothing read it. A second
  tag<->type source of truth that could drift from the serializer's match arms
  — the opposite of the "kill tag-drift" rationale. Kept format_guid (the only
  used part).
- derive #[ps(alias = ..)] and #[ps(default)]: no message uses them; the
  tolerant/multi-spelling cases live in client-core's separate FromParams
  system, not this derive.
- derive #[ps(type_names(..))]: no migrated object needs a <TN> chain yet
  (Coordinates/Size deliberately omit it). Re-add it with the first typed
  sub-object that actually carries type names.

Kept everything with a real consumer: L1 accessors/builder, #[ps(with)]
(session_capability), and the nesting bridge — optional message_type +
generated ToPsValue/FromPsValue (Coordinates/Size). 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
…rsions

New PsEnum derive covers both CLIXML enum encodings, zero hand-written impls:
- repr = "object": full enum <Obj> (<TN> chain + <ToString> variant name +
  <I32> discriminant) — e.g. ApartmentState, PSThreadOptions;
- repr = "i32": bare <I32> primitive — e.g. PSInvocationState,
  RunspacePoolStateValue.
Variants carry explicit discriminants; #[ps(rename)] overrides the ToString
name. Generates ToPsValue/FromPsValue (+ From/TryFrom<ComplexObject> for the
object repr) so the enum composes inside derived structs.

Migrated ApartmentState, PSThreadOptions (object) and PSInvocationState,
RunspacePoolStateValue (i32) to the derive, deleting their hand-written
From/TryFrom/ToPsValue/FromPsValue. Wire bytes unchanged; psrp tests green.

Part of: concrete types + macro-generated conversions, no dynamically
assembled objects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…57644)

Migrate the last two enums (PipelineResultTypes, RemoteStreamOptions) to
#[derive(PsEnum)] object repr. PipelineResultTypes has an `Error` variant that
collided with `TryFrom::Error`, so PsEnum no longer generates TryFrom at all —
it emits an inherent `from_ps_object(ComplexObject) -> Result<Self, _>` plus
ToPsValue/FromPsValue/From. With no `Error` associated item in scope, variant
paths are unambiguous. Updated the two call sites (command/create_pipeline)
from `Enum::try_from(obj)` to `Enum::from_ps_object(obj)`.

All standalone CLIXML enums are now macro-driven (zero hand-written
conversions). Wire bytes unchanged; psrp tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
… `also`

Prep for migrating the cross-crate host param/return types to the derive:
- Ps* derives now emit `ironposh_psrp::...` paths (via `extern crate self as
  ironposh_psrp` in the psrp root) so they work from downstream crates too;
  the Simple* XML derives are untouched.
- Re-add struct attr #[ps(type_names("A","B"))] (now has real consumers: the
  typed .NET host objects that carry a <TN> chain).
- Add field attr #[ps(also = "AltName")]: emit a property under additional
  names and accept any of them on parse — preserves the dual camelCase +
  PascalCase emission of host objects byte-for-byte.

psrp builds and tests green; wire output unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pd7AHzyaL2o4eDWQGBGv6A
…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
RFC #12 (2/5): derive PSRP & host-call messages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants