Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 7 additions & 3 deletions contributing/parallel_safe_session_attach.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,14 @@ If you write a new bootstrap-style file that legitimately needs custom attach, a
1. **Don't inline the literal `%%SESSION_PATH%%` token as a sentinel comparison.** The PowerShell wrapper's `.Replace()` is global, so any occurrence of the literal token will be rewritten. If you need to detect "unsubstituted token," build the comparison string at runtime via `Chr(37) & Chr(37) & "SESSION_PATH" & Chr(37) & Chr(37)`. See `sap_gui_object_details.vbs` for the precedent (and the bug it originally hid).
2. **Include order matters when both attach-lib and session-lock are present.** Attach lib MUST load first because session-lock's pre-unlock popup sweep reads from `oSession`. The canonical pattern above gets this right.
3. **The helper handles ALL error paths.** Don't wrap `AttachSapSession(SESSION_PATH)` in your own `If oSession Is Nothing Then ...` — the helper has already `WScript.Quit 2`'d on failure. Adding your own block is dead code.
4. **`SAPDEV_SESSION_PATH` only reaches cscript if cscript is a CHILD OF THE SAME PowerShell process.** A process env var is inherited by children — it is *not* shared between sibling processes and it dies when the process that set it exits. Most SKILL.md files set it in the **generator** block (`...-Fill the tokens`) and then launch `cscript` from a **separate, later** block, so the variable is already gone: the helper skips Strategy 2 and silently falls through to the sole-connection default. That is not hypothetical — it is what let a run pinned to S4D/100 drive a GUI window on S4H/400 and download the wrong system's source (2026-08-06). **Fix the generator by baking the resolved path into `%%SESSION_PATH%%`** (Strategy 1 — it is a `Const` in the emitted VBS, so it survives any process boundary), and only rely on the env var when the same block that exports it also runs `cscript`.
4. **`SAPDEV_SESSION_PATH` only reaches cscript if cscript is a CHILD OF THE SAME PowerShell process.** A process env var is inherited by children — it is *not* shared between sibling processes and it dies when the process that set it exits. Most SKILL.md files set it in the **generator** block (`...-Fill the tokens`) and then launch `cscript` from a **separate, later** block, so the variable is already gone: the helper skips Strategy 2 and silently falls through to the sole-connection default. That is not hypothetical — it is what let a run pinned to S4D/100 drive a GUI window on S4H/400 and download the wrong system's source (2026-08-06). **Fix the generator by baking the resolved path into `%%SESSION_PATH%%`** (Strategy 1 — it is a `Const` in the emitted VBS, so it survives any process boundary), and only rely on the env var when the same block that exports it also spawns the consumer. CI-enforced as a hard error since 2026-08-06 — see gate 13 below. Every SKILL.md in the repo now bakes; the env var remains a valid Strategy 2 for a caller that spawns `cscript` from its own process.
5. **Declare the expected SAP system whenever the skill also talks RFC.** `Connect-SapRfc` and `AttachSapSession` resolve their target through two *different* chains (pin → GUI-active → default → sole-profile, vs. hint → env → sole-connection → refuse), so they can land on different systems while the skill believes it read one. Export the expectation in the block that launches `cscript`:
```powershell
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
Set-SapGuiTargetExpectation -WorkTemp '{WORK_TEMP}' | Out-Null # sets SAPDEV_EXPECT_SYSTEM/_CLIENT
& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\..._run.vbs'
```
`AssertSapGuiTarget` in the attach lib then refuses (exit 2) any session that is not that system, instead of retargeting silently. Every attach — expectation or not — now also emits `GUI_TARGET: system=… client=… user=… path=… via=…`, so a skill's output always records which SAP system it actually drove. **Surface that line in the skill's report; never state a system you did not read off it.**
`AssertSapGuiTarget` in the attach lib then refuses (exit 2) any session that is not that system, instead of retargeting silently. Every attach — expectation or not — now also emits `GUI_TARGET: system=… client=… user=… path=… via=…`, so a skill's output always records which SAP system it actually drove. **Surface that line in the skill's report; never state a system you did not read off it.** This matters most on the **write** paths — a create/update/delete aimed at the wrong system is not a bad read, it is a bad write — so it is CI-enforced as a hard error for every write-capable skill since 2026-08-06 (gate 14 below). Read-only GUI skills are not gated, but declaring the expectation there is still recommended: the incident that motivated all of this *was* a read.

---

Expand All @@ -202,7 +202,7 @@ to hard errors once their counts ratcheted to zero (cscript/locale-literal
2026-07-10; missing screen baselines 2026-07-24 at 136/136), so violations now
FAIL the run instead of appearing in the OK line.)

On failure, the script lists each non-conforming file with a specific reason. This contract's gate covers twelve conditions (2026-07-03: grown from seven; the authoritative spec for each is its own comment block inside `scripts/check-consistency.mjs` — note the checker has since grown further gates *outside* this contract's scope, e.g. the Rule 0 safety-gate coverage check and the manifest/marketplace alignment checks):
On failure, the script lists each non-conforming file with a specific reason. This contract's gate covers fourteen conditions (2026-07-03: grown from seven; 2026-08-06: +2 for the cross-system target guard; the authoritative spec for each is its own comment block inside `scripts/check-consistency.mjs` — note the checker has since grown further gates *outside* this contract's scope, e.g. the Rule 0 safety-gate coverage check and the manifest/marketplace alignment checks):

1. Legacy `For Each oCandidate In oApp.Children` (and its variant patterns) → must not appear in non-exempt operational VBS.
2. VBS with `Const SESSION_PATH` but no `%%ATTACH_LIB_VBS%%` include → the helper will be undefined at runtime.
Expand All @@ -216,6 +216,10 @@ On failure, the script lists each non-conforming file with a specific reason. Th
10. `references/*.vbs` line that branches on translated GUI text — `InStr(...)` against a curated English literal ("resulted in errors", "locked", "Initial Screen", ...) or an `LCase(<title>) = "..."` compare — outside comments/`WScript.Echo` → **hard error** (promoted from WARN on 2026-07-10; documented multi-locale matchers are exempted via `LOCALE_LITERAL_EXEMPT` — see `shared/rules/language_independence_rules.md`).
11. Shipped `.ps1`/`.vbs` (skills' `references/` + sap-dev-core `shared/scripts/`) containing a non-ASCII byte without a UTF-8 BOM → **hard error** (promoted from WARN on 2026-07-02 once the tree reached zero offenders; use `--`/`->` in comments and `ChrW()` for runtime non-ASCII).
12. A file in `sap-dev-core/shared/scripts` not mentioned in CLAUDE.md's "Current Shared Files" table → **hard error** (added 2026-07-03; the reverse direction — a shared script shrunk to a single same-plugin consumer with no shared-side wiring — draws a shared-placement WARN, exceptions via `SHARED_PLACEMENT_ALLOWLIST`).
13. SKILL.md fenced block that assigns `$env:SAPDEV_SESSION_PATH` but launches **no child process in that same block** → **dead pin** → **hard error** (added 2026-08-06). Each fenced block runs as its own process, so the export dies before the VBS starts, the attach lib finds no env var, and Strategy 3 (sole-connection) silently discards the AI-session pin. Bake the resolved path into `%%SESSION_PATH%%` instead — see gotcha 4 and the canonical wrapper above. A block that *does* spawn a child (`cscript`, or a `powershell -File …` that resolves the session itself, as `/sap-update-addon`'s detect step does) is fine: children inherit the environment.
14. SKILL.md fenced block that invokes `cscript` from a **write-capable** skill (`SAFETY_GATE_SKILLS`) without `Set-SapGuiTargetExpectation` in that same block → **unverified GUI target** → **hard error** (added 2026-08-06). Same process boundary as 13: `SAPDEV_EXPECT_SYSTEM`/`_CLIENT` must be exported by the process that launches `cscript` or `AssertSapGuiTarget` has nothing to enforce, and a GUI parked on another system gets written to instead of refused. See gotcha 5. Blocks whose only VBS targets are in `TIER3_EXEMPT_VBS` are skipped — those templates never call `AttachSapSession`, so the expectation would be inert (the pre-login `sap_check_gui_login_status.vbs` probe, the `sap_gui_security_warmup.vbs` Hardcopy, `/sap-gui-probe`'s self-resolving drivers).

> **Ratchet history for 13/14.** Both shipped 2026-08-06 as WARN over 102 offenders (58 dead pins + 44 unverified write blocks), hard-erroring only the six flagship deploy skills (se38 / se37 / se11 / se24 / se91 / se21). The remaining 34 skills were migrated the same day, both counts reached zero, and both halves were promoted to unconditional hard errors — same path the bare-cscript and screen-baseline gates took.

---

Expand Down
26 changes: 18 additions & 8 deletions plugins/sap-dev-core/skills/sap-activate-object/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,16 @@ $content = $content -replace '%%OBJECT_NAME%%','THE_NAME'
$content = $content -replace '%%OBJECT_TYPE%%','THE_TYPE' # SE11 only
$content = $content -replace '%%ACTIVATION_LOG_VBS%%','<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_activation_log.vbs' # SE11 only
$content = $content -replace '%%TEMP_DIR%%','{RUN_TEMP}' # SE11 only
# Phase 3.5 session-attach plumbing.
$sessionPath = ''
$content = $content -replace '%%SESSION_PATH%%', $sessionPath
$content = $content -replace '%%ATTACH_LIB_VBS%%','<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_attach_lib.vbs'
# Phase 4.2 session-attach plumbing. BAKE the resolved path into %%SESSION_PATH%%
# (attach Strategy 1): this generator is a SEPARATE process from the one that runs
# cscript, so an $env:SAPDEV_SESSION_PATH exported here dies with it and the attach
# lib silently falls through to its sole-connection default (2026-08-06).
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$sessionPath = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$content = $content.Replace('%%SESSION_PATH%%', $sessionPath)
$content = $content -replace '%%ATTACH_LIB_VBS%%','<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_attach_lib.vbs'
[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_activate_<TXN>_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
Write-Host 'Done'
Write-Host ("Done (session_path='" + $sessionPath + "')")
```

> **Activation-log capture (SE11 only, by design)**: when SE11 activation
Expand All @@ -264,10 +266,18 @@ Write-Host 'Done'
> those VBS templates is the right surfacing mechanism. Do NOT propagate
> this helper to non-SE11 activate scripts.

Run via 32-bit cscript:
Generate:
```bash
powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_activate_<TXN>_run.ps1"
C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_activate_<TXN>_run.vbs
```

Run via 32-bit cscript, declaring the GUI target in the SAME block (the attach
lib reads `SAPDEV_EXPECT_SYSTEM`/`_CLIENT` from the process environment, so a GUI
parked on a different system than the RFC leg is refused, not activated):
```powershell
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
Set-SapGuiTargetExpectation -WorkTemp '{WORK_TEMP}' | Out-Null
& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_activate_<TXN>_run.vbs'
```

Each VBS emits:
Expand Down
38 changes: 22 additions & 16 deletions plugins/sap-dev-core/skills/sap-atc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,14 @@ $content = $content.Replace('%%OBJECT_NAME%%', 'THE_OBJECT_NAME') # empt
# Batch mode (A5): absolute path to the <TYPE> <NAME> object-list file, else empty.
$content = $content.Replace('%%OBJECT_LIST_FILE%%','THE_OBJECT_LIST_FILE')
$content = $content.Replace('%%SESSION_LOCK_VBS%%',"$shared\scripts\sap_session_lock.vbs")
# Phase 3.5 session-attach plumbing.
$sessionPath = ''
# Phase 4.2 session-attach plumbing. BAKE the resolved path into %%SESSION_PATH%%
# (attach Strategy 1): this generator is a SEPARATE process from the one that runs
# cscript, so an $env:SAPDEV_SESSION_PATH exported here dies with it and the attach
# lib silently falls through to its sole-connection default (2026-08-06).
. "$shared\scripts\sap_connection_lib.ps1"
$sessionPath = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$content = $content.Replace('%%SESSION_PATH%%', $sessionPath)
$content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs")
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage1_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
```

Expand Down Expand Up @@ -316,11 +318,12 @@ $content = $content.Replace('%%CHECK_VARIANT%%', 'THE_CHECK_VARIANT')
$content = $content.Replace('%%OBJECT_PROVIDER%%', 'THE_OBJECT_PROVIDER')
$content = $content.Replace('%%SESSION_LOCK_VBS%%', "$shared\scripts\sap_session_lock.vbs")
# Phase 3.5 session-attach plumbing.
$sessionPath = ''
# BAKE the session path (attach Strategy 1) -- an env var exported by this
# generator process never reaches the separate process that runs cscript.
. "$shared\scripts\sap_connection_lib.ps1"
$sessionPath = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$content = $content.Replace('%%SESSION_PATH%%', $sessionPath)
$content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs")
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage2_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
```

Expand Down Expand Up @@ -374,11 +377,12 @@ $content = [System.IO.File]::ReadAllText("$skillDir\references\sap_atc_check_ru
$content = $content.Replace('%%RUN_SERIES_NAME%%', 'THE_RUN_SERIES')
$content = $content.Replace('%%SESSION_LOCK_VBS%%', "$shared\scripts\sap_session_lock.vbs")
# Phase 3.5 session-attach plumbing.
$sessionPath = ''
# BAKE the session path (attach Strategy 1) -- an env var exported by this
# generator process never reaches the separate process that runs cscript.
. "$shared\scripts\sap_connection_lib.ps1"
$sessionPath = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$content = $content.Replace('%%SESSION_PATH%%', $sessionPath)
$content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs")
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage3_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
```

Expand Down Expand Up @@ -456,11 +460,12 @@ $content = $content.Replace('%%RUN_SERIES_NAME%%', 'THE_RUN_SERIES')
$content = $content.Replace('%%OUTPUT_PATH%%', 'THE_OUTPUT_PATH')
$content = $content.Replace('%%SESSION_LOCK_VBS%%', "$shared\scripts\sap_session_lock.vbs")
# Phase 3.5 session-attach plumbing.
$sessionPath = ''
# BAKE the session path (attach Strategy 1) -- an env var exported by this
# generator process never reaches the separate process that runs cscript.
. "$shared\scripts\sap_connection_lib.ps1"
$sessionPath = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$content = $content.Replace('%%SESSION_PATH%%', $sessionPath)
$content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs")
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage4_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
```

Expand Down Expand Up @@ -581,11 +586,12 @@ $content = $content.Replace('%%RUN_SERIES_NAME%%', 'THE_RUN_SERIES')
$content = $content.Replace('%%OUTPUT_PATH%%', 'THE_DRILL_PATH')
$content = $content.Replace('%%SESSION_LOCK_VBS%%',"$shared\scripts\sap_session_lock.vbs")
# Phase 3.5 session-attach plumbing.
$sessionPath = ''
# BAKE the session path (attach Strategy 1) -- an env var exported by this
# generator process never reaches the separate process that runs cscript.
. "$shared\scripts\sap_connection_lib.ps1"
$sessionPath = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$content = $content.Replace('%%SESSION_PATH%%', $sessionPath)
$content = $content.Replace('%%ATTACH_LIB_VBS%%', "$shared\scripts\sap_attach_lib.vbs")
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_atc_stage4b_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
```

Expand Down
27 changes: 19 additions & 8 deletions plugins/sap-dev-core/skills/sap-change-package/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,20 +275,31 @@ $content = $content -replace '%%OBJECT_TYPE%%','THE_TYPE' # SE11 only
$content = $content -replace '%%NEW_PACKAGE%%','THE_PKG'
$content = $content -replace '%%TRANSPORT%%','THE_TR'
$content = $content -replace '%%TR_DESCRIPTION%%','THE_TR_DESC'
# Phase 3.5 session-attach plumbing.
$sessionPath = ''
$content = $content -replace '%%SESSION_PATH%%', $sessionPath
$content = $content -replace '%%ATTACH_LIB_VBS%%','<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_attach_lib.vbs'
# Phase 4.2 session-attach plumbing. BAKE the resolved path into %%SESSION_PATH%%
# (attach Strategy 1): this generator is a SEPARATE process from the one that runs
# cscript, so an $env:SAPDEV_SESSION_PATH exported here dies with it and the attach
# lib silently falls through to its sole-connection default (2026-08-06).
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$sessionPath = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
$content = $content.Replace('%%SESSION_PATH%%', $sessionPath)
$content = $content -replace '%%ATTACH_LIB_VBS%%','<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_attach_lib.vbs'
[System.IO.File]::WriteAllText('{RUN_TEMP}\sap_change_package_<TXN>_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
Write-Host 'Done'
Write-Host ("Done (session_path='" + $sessionPath + "')")
```

Run via 32-bit cscript:
Generate:
```bash
powershell -ExecutionPolicy Bypass -File "{RUN_TEMP}\sap_change_package_<TXN>_run.ps1"
C:/Windows/SysWOW64/cscript.exe //NoLogo {RUN_TEMP}\sap_change_package_<TXN>_run.vbs
```

Run via 32-bit cscript, declaring the GUI target in the SAME block (the attach
lib reads `SAPDEV_EXPECT_SYSTEM`/`_CLIENT` from the process environment, so a GUI
parked on a different system than the RFC leg is refused rather than having its
namesake object reassigned):
```powershell
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
Set-SapGuiTargetExpectation -WorkTemp '{WORK_TEMP}' | Out-Null
& 'C:/Windows/SysWOW64/cscript.exe' //NoLogo '{RUN_TEMP}\sap_change_package_<TXN>_run.vbs'
```

Each VBS emits a stable contract:
Expand Down
Loading