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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ was deleted the same day.
| `shared/scripts/sap_log_lib.vbs` | **All VBScript skill scripts (optional)** | Structured logger. Include via `ExecuteGlobal FSO.OpenTextFile("%%LOG_LIB_VBS%%",1).ReadAll()`. Functions: `LogStart(skill, paramsArray)`, `LogStep(runId, level, step, msg)`, `LogEnd(runId, status, exitCode, errorMsg)`. Same JSONL/TSV/TEXT formats and redaction as the PS lib. Writes UTF-8 (no BOM) via ADODB.Stream so files concatenate cleanly with PS-emitted lines. |
| `shared/scripts/sap_session_lock.vbs` | **All GUI-scripting VBS reference scripts that perform multi-step writes (mandatory per Rule 7)** | Session-lock helpers. Include via `ExecuteGlobal FSO.OpenTextFile("%%SESSION_LOCK_VBS%%",1).ReadAll()`. Functions: `TryLockSession(sess)` → returns Boolean (False if API unavailable on this SAP GUI build); `ReleaseSession(sess, wasLocked)` — idempotent unlock that ALSO sweeps up to 5 chained orphan modal popups via `sendVKey 12` (F12 / Cancel) before unlocking, so the user never gets a frozen popup on session handover. Wrap source-paste / save / activate / popup-driving critical sections to block in-session focus stealing. Pair with the existing AppActivate-loop guards for SendKeys-based pastes (defence in depth: AppActivate blocks external focus stealing, LockSessionUI blocks internal, the pre-unlock sweep covers leftover modals). |
| `shared/scripts/sap_delete_popups.vbs` | **The delete VBS of sap-se37 / sap-se11 / sap-se24 / sap-se38 / sap-se21** | Shared post-delete popup walker. Included by deriving its path from the already-substituted `%%ATTACH_LIB_VBS%%` token (same dir, so no extra generator token): `sDpDir = oDpFso.GetParentFolderName("%%ATTACH_LIB_VBS%%") : ExecuteGlobal oDpFso.OpenTextFile(oDpFso.BuildPath(sDpDir, "sap_delete_popups.vbs"),1).ReadAll()`. Exposes `Function WalkDeletePopups(oSession, objdirPkg, objdirLang, sapTr)` → walks the active window (cap 10), dispatching each modal by DDIC control id ONLY (locale-independent): SAPLSETX language (`ctxtRSETX-MASTERLANG`/`btnPUSH1`), KO007 "Create Object Directory Entry" (ECC6 — fill empty package from `objdirPkg` + 1-char `objdirLang`, else accept pre-filled, else Local Object `btn[7]`), TR prompt (`ctxtKO008-TRKORR`; returns `"ABORT_EMPTY_TR"` when `sapTr` is empty so the caller releases its lock + `WScript.Quit 1`), and a confirm cascade (`btnSPOP-OPTION1` / `btnBUTTON_1` / `tbar[0]/btn[0]` / Enter). Each branch is gated by its control id, so the union is a strict superset of every per-skill loop it replaced and cannot misfire on a screen lacking that control. Pure function library (receives an already-attached `oSession`; does NOT bind the Scripting engine / declare `SESSION_PATH` / include the attach lib / call `AttachSapSession`), so — like `sap_session_lock.vbs` — it is not a "driving" VBS and lives in `shared/scripts/`, outside the `skills/*/references/` scan scope of `scripts/check-consistency.mjs` (no baseline required). se19 (classic + new) and cmod keep their own popup handling (divergent `For pass` / sequential structure + lenient-TR semantics). |
| `shared/scripts/sap_attach_lib.vbs` | **All GUI-scripting VBS reference scripts that drive SAP GUI** — mandatory for the Tier 3 (parallel-safe session attach) contract. | Shared session-attach primitive — **multi-connection aware (Phase 3.5)** + **pin-file-free (Phase 4.2)**. Include via `ExecuteGlobal FSO.OpenTextFile("%%ATTACH_LIB_VBS%%",1).ReadAll()`. Exposes `Function AttachSapSession(sHint)` which resolves the target session in this order: (1) `sHint` (typically the `%%SESSION_PATH%%` token from the calling wrapper); (2) `SAPDEV_SESSION_PATH` env var — set by the SKILL.md wrapper to `Get-SapCurrentSessionPath`'s return; (3) sole-connection + sole-session safe default; (4) **refuse loud** with `ERROR: N SAP connections attached; cannot pick one safely. Run /sap-login to pin a connection, or pass --session ...`. **Strategies 1 and 2 also work cross-connection** — they take full `/app/con[N]/ses[M]` paths and never silently retarget. Strategies 3 and 4 keep single-connection callers simple while multi-connection callers get safe refusal instead of silent miss-targeting. The convention: each migrated VBS declares `Const SESSION_PATH = "%%SESSION_PATH%%"`, includes this lib, and calls `Set oSession = AttachSapSession(SESSION_PATH)`. Calling skill wrappers (PowerShell) substitute `%%SESSION_PATH%%` with the parsed `--session` argument (or empty), `%%ATTACH_LIB_VBS%%` with the absolute path to this file, AND set `$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (from `sap_connection_lib.ps1`) so the AI session's pin propagates. Unsubstituted-token sentinel via a `Chr(37)`-built runtime string so global wrapper substitution cannot corrupt the comparison. Pairs with the broker: the broker decides which session belongs to this AI session; the helper lets every VBS attach to that decision safely. |
| `shared/scripts/sap_attach_lib.vbs` | **All GUI-scripting VBS reference scripts that drive SAP GUI** — mandatory for the Tier 3 (parallel-safe session attach) contract. | Shared session-attach primitive — **multi-connection aware (Phase 3.5)** + **pin-file-free (Phase 4.2)**. Include via `ExecuteGlobal FSO.OpenTextFile("%%ATTACH_LIB_VBS%%",1).ReadAll()`. Exposes `Function AttachSapSession(sHint)` which resolves the target session in this order: (1) `sHint` (typically the `%%SESSION_PATH%%` token from the calling wrapper); (2) `SAPDEV_SESSION_PATH` env var — set by the SKILL.md wrapper to `Get-SapCurrentSessionPath`'s return; (3) sole-connection + sole-session safe default; (4) **refuse loud** with `ERROR: N SAP connections attached; cannot pick one safely. Run /sap-login to pin a connection, or pass --session ...`. **Strategies 1 and 2 also work cross-connection** — they take full `/app/con[N]/ses[M]` paths and never silently retarget. Strategies 3 and 4 keep single-connection callers simple while multi-connection callers get safe refusal instead of silent miss-targeting. The convention: each migrated VBS declares `Const SESSION_PATH = "%%SESSION_PATH%%"`, includes this lib, and calls `Set oSession = AttachSapSession(SESSION_PATH)`. Calling skill wrappers (PowerShell) substitute `%%SESSION_PATH%%` with the parsed `--session` argument (or empty), `%%ATTACH_LIB_VBS%%` with the absolute path to this file, AND set `$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'` (from `sap_connection_lib.ps1`) so the AI session's pin propagates. Unsubstituted-token sentinel via a `Chr(37)`-built runtime string so global wrapper substitution cannot corrupt the comparison. Pairs with the broker: the broker decides which session belongs to this AI session; the helper lets every VBS attach to that decision safely. **Target stamp + assertion (2026-08-06)**: every success path now echoes `GUI_TARGET: system=<SID> client=<NNN> user=<U> path=<...> via=<strategy>` (read from `GuiSession.Info` — IDs, not localised text), and when the wrapper exports `SAPDEV_EXPECT_SYSTEM` / `SAPDEV_EXPECT_CLIENT` a mismatch is a **hard refusal** (exit 2) instead of a silent retarget. Set them via `Set-SapGuiTargetExpectation` (`sap_connection_lib.ps1`), which resolves the *same* profile `Connect-SapRfc` picks — so the GUI leg and the RFC leg of a skill can no longer land on different SAP systems. **Both must be exported in the same process that launches `cscript`** (env vars die with the generator block). Unset = legacy behaviour, still stamped. |
| `shared/scripts/sap_session_broker.ps1` | **All GUI-scripting skills that may run in parallel** (today: `/sap-gui-skill-scaffold` parallel path + the 4 Phase-3.1 migrated read-only skills; will become broadly mandatory after Tier 3 migration). Full contract: `shared/rules/sap_session_broker.md`. | SAP GUI Session Broker — **multi-connection aware (v2 schema, Phase 3.5)**. PowerShell, ~600 LOC. Single-binary CLI with five actions — `acquire` / `release` / `discover` / `gc` / `list` — driven by `-Action <name> -WorkTemp <abs-path>` + per-action args. State lives in `{WORK_TEMP}\session_registry.json` (UTF-8 no BOM, nested `connections[]` shape); cross-process concurrency serialized by a named Windows mutex (`SapDevSessionBroker_v2`) acquired through `System.Threading.Mutex` with a 10s timeout for crash recovery. **Connection isolation**: a claim resolved against connection N never returns a session of connection M. Reactive cleanup + identity-reconciliation sweep runs inside every acquire/release/discover/gc across ALL connections: it mirrors live SAP identity onto each block (live is source of truth) then drops entries on these failure modes — session closed, owner PID dead, TTL expired, relogin, entire connection closed. A reused `/app/con[N]` slot now hosting a DIFFERENT system is detected by the `(system,client,user)` tuple — NOT `SystemSessionId`, which on the tested kernels is per-workstation, not per-logon, and stays identical across an A→B swap on one slot (the 2026-06-07 stale-identity bug); on a tuple change the block is reset to the live identity and its stale `connection_id` cleared (re-bound on next finalize). Idempotent on re-acquire by `task_id`. Connection-targeting acquire args (Phase 4.1+): broker auto-resolves `-AiSessionId` via parent-PID walk and reads its `ai_sessions[<id>].connection_id` pin. Explicit `-SessionPath` / `-ConnectionPath` / `-SystemName -Client -User` still override; resolution falls through to sole-connection auto-default or DENIED. Spawns on demand on the target connection via `/oSESSION_MANAGER` (the only OK-code mechanism verified on S/4HANA 1909 kernel 754; `CreateSession` and bare `/o` no-op). Stdout last line: `ACQUIRED: path=<p> sessionNumber=<n> connection=<c> reused=<bool>` / `RELEASED: path=<p> connection=<c>` / `NOT_FOUND` / `DENIED: <reason>` (exit 1) / `ERROR: <reason>` (exit 2). Auto-rebuilds a v1 registry on first call after upgrade with a `WARN: v1 registry detected` line. Shells out to `sap_session_broker_com.vbs` for every SAP-side operation because PowerShell 7+/.NET 5+ cannot bind the SAP GUI Scripting Engine directly (`Marshal::GetActiveObject` removed in .NET 5+; even 32-bit Windows PowerShell 5.1 fails to resolve the SAPGUI ProgID through the ROT). |
| `shared/scripts/sap_session_broker_com.vbs` | **Internal helper for `sap_session_broker.ps1`** — not intended for direct calls by other skills. | SAP COM helper for the broker. VBScript run via 32-bit `cscript`. Single argv command + JSON-on-stdout protocol. **Multi-connection aware** (Phase 3.5): `INFO` returns ALL attached SAP connections (each with `connection_path` / `description` / `system_name` / `client` / `user` / `language` / `logon_id` + a `sessions[]` array); `SPAWN <connection_path>` spawns on a SPECIFIC connection (drives `/n` + `/oSESSION_MANAGER` on that connection's anchor, returns the newcomer's path + `SessionNumber`); `RESET <session_path>` drives `/n` on a specific session (used by `release` to return to SAP Easy Access); `PROBE <session_path>` does a single-session `findById` + `Info` read (used by acquire's pre-allocation Easy-Access verification). Exit codes: 0 success, 1 usage error, 2 SAP-unreachable, 3 command-level failure (details in JSON `error` field). JSON output is one line per invocation — broker parses with `ConvertFrom-Json`. |
| `shared/scripts/sap_activation_log.vbs` | **SE11 / DDIC GUI-scripting VBS only — do NOT include in SE38/SE37/SE24/SE91 (no equivalent menu in those transactions)** | Activation-log capture. Include via `ExecuteGlobal FSO.OpenTextFile("%%ACTIVATION_LOG_VBS%%",1).ReadAll()`. Functions: `CaptureActivationLog(oSess, sObjectName, sOutDir, kEnter, kBack)` → returns "" on failure or absolute path of saved log file on success; `ExtractTopActivationError(sLogPath)` → returns the top error line from the log (empty string if none). After Activate, when `sbar.MessageType = "E"` or `"A"`, call `CaptureActivationLog` then echo `ACTIVATION_LOG: <path>` and `ACTIVATION_ERROR: <top-error>` so the operator sees the specific failure instead of the generic "refer to log" SAP popup. Walks Utilities > Activation Log → Log > Save Local File via menu indices captured from a SAP GUI recording of the SE11 activation-log walk (`Record_SE11_ActivateErrorLog_01.vbs`, S/4HANA 1909). Re-record on releases that move the menus. The `Utilities > Activation Log` menu is a DDIC-worklist concept and exists ONLY in SE11; SE38/SE37/SE24/SE91 surface activation errors inline in the source-code editor + status bar (read via `wnd[0]/sbar.Text` — already done in those skills). |
Expand Down
23 changes: 17 additions & 6 deletions contributing/parallel_safe_session_attach.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,15 @@ $content = $content -replace '%%SOME_PARAM%%','THE_SOME_PARAM'
# ... other parameter substitutions ...

# Phase 4.2 session-attach plumbing.
$sessionPath = '' # set to the parsed --session value if supplied
$content = $content -replace '%%SESSION_PATH%%', $sessionPath
$content = $content -replace '%%ATTACH_LIB_VBS%%','<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_attach_lib.vbs'
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1'
$env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
# Prefer the parsed --session value; otherwise resolve this AI session's pin.
# BAKE it into the VBS rather than exporting $env:SAPDEV_SESSION_PATH here: this
# generator is usually a DIFFERENT process from the one that later runs cscript,
# so an env var set here never arrives (see gotcha 4).
$sessionPath = $ParsedSessionArg
if (-not $sessionPath) { $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_<skill>_<mode>_run.vbs', $content, [System.Text.UnicodeEncoding]::new($false, $true))
```
Expand All @@ -138,7 +142,7 @@ $env:SAPDEV_SESSION_PATH = Get-SapCurrentSessionPath -WorkTemp '{WORK_TEMP}'
each other's `*_run.vbs` between write and `cscript` exec. See CLAUDE.md
"Work Directory Configuration".

- `$sessionPath = ''` is intentional default — the helper auto-resolves via `SAPDEV_SESSION_PATH` → sole-connection → refuse.
- An empty `$sessionPath` is a valid outcome, not a bug — the helper then falls through to sole-connection → refuse. What is NOT acceptable is leaving it hardcoded to `''` while a pin exists: that discards the pin and hands the run to whatever GUI window happens to be open.
- `Get-SapCurrentSessionPath` reads `session_registry.json`'s `ai_sessions[<id>].connection_id` for this AI session (parent-PID walk), finds the matching connection block, returns a usable session path on it. Empty string when nothing resolves; the attach lib's sole-connection fallback or "refuse" path then takes over.
- If the SKILL.md is wrapping a write-class skill that also includes `%%SESSION_LOCK_VBS%%`, leave that substitution in — it's complementary, not redundant.

Expand Down Expand Up @@ -170,7 +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` is set in PowerShell, read by cscript.** Process env vars cross the boundary, so the cscript child inherits it. Don't try to pass it as an argv arg — the helper specifically looks at the env var.
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`.
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.**

---

Expand Down
Loading