Enforce the declared SAP target on the RFC transport - #9
Merged
Conversation
d7942b5 gave the GUI transport a target guard: every attach stamps GUI_TARGET: and AssertSapGuiTarget hard-refuses (exit 2) when SAPDEV_EXPECT_SYSTEM / SAPDEV_EXPECT_CLIENT are set and mismatch; 01efb00 wired the expectation into every write block. The RFC transport had no equivalent: Connect-SapRfc silently connected to whatever profile its resolution chain (pin -> GUI-active -> default -> sole-profile) landed on, and nothing in its output said which system that was. Live incident 2026-08-06 (second cross-system contamination, RFC transport this time): the m365-copilot adaptive driver spawned a headless Claude session; the child's CLAUDE_CODE_SESSION_ID made its skills resolve a fresh pinless AI session, Connect-SapRfc fell through to the saved DEFAULT profile (S4G = S4H/400), and the session faithfully read Z_EXCEPTION_1 from S4H/400 while the driver's pin, syntax check and verdict all pointed at S4D/100. The staged "fix" was the wrong system's source and still passed the S4D syntax gate (it compiles on both). The m365 side now hands the child the driver's session id via SAPDEV_AI_SESSION_ID (which Get-SapAiSessionId already honors), fixing the resolution -- but nothing ENFORCED the expectation on this transport, so a future resolution drift would again be silent. Fixes: - sap_rfc_lib.ps1: Connect-SapRfc mirrors AssertSapGuiTarget. When the expectation is exported and the resolved target mismatches, it refuses hard (ERROR naming both systems, "continuing would read/write TWO DIFFERENT SAP SYSTEMS in one run", + $null return) -- BEFORE logon when the identity is known pre-connect, so the wrong system is never even logged onto with the profile's saved password. Every successful connect stamps RFC_TARGET: system=<SID> client=<NNN> user=<U> endpoint=<..> via=<v> (via = pin | gui-active | default | single-profile | explicit-params), the sibling of GUI_TARGET:, so transcripts carry provenance for both transports. Unset expectation = legacy behaviour, still stamped. - Identity comparison uses the resolved profile's system_name/client, never RfcDestination.SystemID -- that is the configured R3NAME and is blank on a direct -Server/-Sysnr connection (the d7942b5 trap). A caller-supplied endpoint is attributed by exact endpoint match against the saved connection store (so the post-activate verifiers, which resolve the pinned profile themselves and pass -Server explicitly, stay checkable before logon), then by the live logon identity in RfcDestination.SystemAttributes (best-effort, the same read sap_rfc_read_source.ps1 ships); expectation set + no identity readable = refusal, mirroring the GUI side's unverified-target rule. Deliberate cross-system legs (/sap-compare, /sap-transport-sequencer, /sap-cc-* source reads) set no expectation and are unaffected; under an inherited expectation they must clear the pair first (Set-SapGuiTargetExpectation -Clear). - sap_connection_lib.ps1: Get-SapCurrentConnectionProfile gains an optional [ref] -ResolvedVia out-param reporting which resolution step won -- the stamp's via= is exactly what the incident forensics were missing (a child resolving via=default while the run believed it was pinned). Additive; all existing callers use named parameters. Set-SapGuiTargetExpectation docs + console lines now state that BOTH transports enforce the pair (children inherit the environment, so one export covers every RFC call in the same process tree). Verified offline (NCo stubbed, no logon possible): pre-logon refusal on system and on client mismatch, via=gui-active provenance through the [ref], endpoint-match attribution refusing an explicit -Server call under a bogus expectation, matching expectations passing, and the no-expectation legacy path untouched. Verified live (32-bit PS, read-only connect+ping on S4D/100): RFC_TARGET stamps system=S4D client=100 endpoint=sap1.vicp.cc:70 via=gui-active, SystemAttributes.SystemID reads 'S4D' on a direct connection, and the function still returns a clean RfcDestination (the stamp is Write-Host, not pipeline output). Full log: temp/testReport/ rfc_target_guard_20260806.md (local). npm run validate green. Docs: gotcha 5 in parallel_safe_session_attach.md grows the RFC side; CLAUDE.md rows for sap_rfc_lib.ps1 / sap_attach_lib.vbs updated; the attach-lib header cross-references the RFC mirror. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KexiHe
added a commit
that referenced
this pull request
Aug 6, 2026
…te assert Get-SapCurrentConnectionProfile's new out-parameter was declared [ref]$ResolvedVia = $null (PR #9). Windows PowerShell 5.1 throws ParameterArgumentTransformationError converting the $null default for every caller that omits the argument; pwsh 7 accepts it -- the fourth instance of the works-in-pwsh-7 portability class. Connect-SapRfc passes the argument and kept working, so read paths looked healthy while every write skill's Rule 0 assert under 32-bit PS 5.1 (the production driver shape) threw, was caught, and reported a false "no pin, no default, no single-profile bootstrap" refusal against a valid pin. The parameter is now untyped (callers still pass ([ref]$var)) and all four assignments guard on -is [ref]. Verified live on PS 5.1 and pwsh 7 with a pinned S4D/100 profile: assert returns SAFETY: ALLOW on both. The bug failed closed -- it refused work it should have allowed, never the reverse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Why
d7942b5 / #7 gave the GUI transport a target guard (
GUI_TARGET:stamp +AssertSapGuiTargethard refusal onSAPDEV_EXPECT_SYSTEM/_CLIENTmismatch), and #8 wired the expectation into every write block. The RFC transport had no equivalent:Connect-SapRfcsilently connected to whatever profile its resolution chain (pin → GUI-active → default → sole-profile) landed on, and nothing in its output said which system that was.Live incident 2026-08-06 (second cross-system contamination, RFC transport this time): the m365-copilot adaptive driver spawned a headless Claude session; the child's
CLAUDE_CODE_SESSION_IDresolved a fresh pinless AI session,Connect-SapRfcfell through to the saved DEFAULT profile (S4G = S4H/400), and the session faithfully readZ_EXCEPTION_1from S4H/400 while the driver's pin, syntax check and verdict all pointed at S4D/100. The staged "fix" was the wrong system's source and still passed the S4D syntax gate. The m365 side now hands the child the driver's session id viaSAPDEV_AI_SESSION_ID, which fixes the resolution — this PR makes any future drift refuse loudly instead of contaminating silently.What
sap_rfc_lib.ps1—Connect-SapRfcmirrorsAssertSapGuiTarget:ERRORnaming both systems, "continuing would read/write TWO DIFFERENT SAP SYSTEMS in one run",$nullreturn) — before logon when the identity is known pre-connect, so the wrong system is never even logged onto with the profile's saved password.RFC_TARGET: system=<SID> client=<NNN> user=<U> endpoint=<...> via=<pin|gui-active|default|single-profile|explicit-params>— the sibling ofGUI_TARGET:, so transcripts carry provenance for both transports. Unset expectation = legacy behaviour, still stamped.system_name/client— neverRfcDestination.SystemID(configured R3NAME, blank on direct-Server/-Sysnrconnections; the d7942b5 trap). Caller-supplied endpoints are attributed by exact endpoint match against the saved connection store (keeps the post-activate verifiers checkable pre-logon), then by the liveSystemAttributeslogon identity; expectation set + no identity readable ⇒ unverified-target refusal, mirroring the GUI rule.sap_connection_lib.ps1—Get-SapCurrentConnectionProfilegains an optional[ref] -ResolvedViaout-param (which resolution step won); additive, all callers use named params.Set-SapGuiTargetExpectationdocs/messages now state both transports enforce the pair (children inherit the env, so one export covers the whole process tree)./sap-compare,/sap-transport-sequencer,/sap-cc-*source reads) set no expectation and are unaffected; under an inherited expectation they mustSet-SapGuiTargetExpectation -Clearfirst (documented).contributing/parallel_safe_session_attach.mdgrows the RFC side; CLAUDE.md rows updated; attach-lib header cross-references the mirror.Testing
_Load-SapNcostubbed — no logon possible): pre-logon refusal on system and on client mismatch (via=gui-activeprovenance), endpoint-match attribution refusing an explicit-Servercall under a bogus expectation, matching expectations passing, no-expectation legacy path untouched.RFC_TARGET: system=S4D client=100 user=... endpoint=sap1.vicp.cc:70 via=gui-active;SystemAttributes.SystemIDreads'S4D'on a direct connection; return value remains a cleanRfcDestination(stamp isWrite-Host, not pipeline output). Full log:temp/testReport/rfc_target_guard_20260806.md(local).npm run validategreen:OK: 4 plugins, 123 skills, all manifests aligned at version 0.8.1, Tier 3 attach contract clean, screen-baseline coverage 136/136.🤖 Generated with Claude Code