Opens a SAP GUI connection and logs in using SAP GUI Scripting. Multi-profile connection store (Phase 4): save multiple SAP connections (different SID / Client / User / endpoint) at `{work_dir}\runtime\connections.json`, with passwords DPAPI-encrypted at rest. Picks the right one for this AI session via a 4-step identity compare and an AI-session pin. Also verifies SAP NCo 3.1 RFC connectivity (direct or load-balanced via MessageServer + LogonGroup + SystemID). Supports three connection metho...
Scanned 5/27/2026
Install via CLI
openskills install sapdev-ai/sap-dev---
name: sap-login
description: |
Opens a SAP GUI connection and logs in using SAP GUI Scripting.
Multi-profile connection store (Phase 4): save multiple SAP connections
(different SID / Client / User / endpoint) at `{work_dir}\runtime\connections.json`,
with passwords DPAPI-encrypted at rest. Picks the right one for this
AI session via a 4-step identity compare and an AI-session pin.
Also verifies SAP NCo 3.1 RFC connectivity (direct or load-balanced via
MessageServer + LogonGroup + SystemID).
Supports three connection methods: SAP Logon pad entry name (OpenConnection),
load-balanced /M/<msrv>/G/<grp>/S/<sid> string, and direct /H/<host>/S/<port>.
Checks existing sessions first; reuses the active connection when it
matches the saved default.
Prerequisites: SAP GUI installed, SAP GUI Scripting enabled (client + server).
argument-hint: "[--list | --add | --switch <id> | --set-default <id> | --delete <id>]"
---
# SAP GUI Login Skill
You open a SAP GUI connection and log in via SAP GUI Scripting, and optionally
verify RFC connectivity via SAP NCo 3.1.
Task: $ARGUMENTS
---
## Shared Resources
| File | Token | Purpose |
|---|---|---|
| `<SAP_DEV_CORE_SHARED_DIR>/rules/skill_operating_rules.md` | *(rule)* | Mandatory operating rules |
| `<SAP_DEV_CORE_SHARED_DIR>/rules/language_independence_rules.md` | *(rule)* | GUI-scripting language independence — identify by component ID + DDIC field name, status-bar checks via `MessageType` codes (S/W/E/I/A), VKey instead of menu-text, no branching on `.Text`/`.Tooltip`/window titles |
| `sap-dev-core/shared/scripts/sap_check_gui_login_status.vbs` | *(none — static)* | Check session status |
| `sap-dev-core/shared/scripts/sap_login.vbs` | *(template)* | SAP GUI login VBScript. Tokens: `%%SAP_LOGON_DESCRIPTION%%`, `%%SAP_APPLICATION_SERVER%%`, `%%SAP_SYSTEM_NUMBER%%`, `%%SAP_MESSAGE_SERVER%%`, `%%SAP_LOGON_GROUP%%`, `%%SAP_SYSTEM_ID%%`, `%%SAP_SYSTEM_NAME%%`, `%%SAP_CLIENT%%`, `%%SAP_USER%%`, `%%SAP_PASSWORD%%`, `%%SAP_LANGUAGE%%`. |
| `sap-dev-core/shared/scripts/sap_rfc_connect.ps1` | *(template)* | SAP NCo 3.1 RFC connection PowerShell. Now supports load-balanced login via `MessageServer + LogonGroup + SystemID`. |
| `sap-dev-core/shared/scripts/sap_rfc_lib.ps1` | `%%RFC_LIB_PS1%%` | NCo helpers. `Connect-SapRfc` accepts either direct (`-Server` + `-Sysnr`) or load-balanced (`-MessageServer` + `-LogonGroup` + `-SystemID`). |
| `sap-dev-core/shared/scripts/sap_dpapi.ps1` | *(none — static)* | DPAPI encrypt/decrypt for passwords at rest. CLI mode: `-Action protect|unprotect -Value <text>`. |
| `sap-dev-core/shared/scripts/sap_connection_lib.ps1` | *(none — dot-source)* | **Multi-profile connection store**. 4-step identity compare, dedup-on-save, DPAPI password handling, legacy-settings migration. Storage: `{work_dir}\runtime\connections.json`. |
| `sap-dev-core/shared/scripts/sap_session_broker.ps1` | *(none — invoke)* | Broker. New Phase-4 actions: `pin`, `unpin`, `set-connection-id`, `stuck`. New flags: `-AiSessionId`, `-WasCreated`, `-ForceUnpin`. |
| `sap-dev-core/shared/scripts/sap_rfc_system_info.ps1` | *(none — direct invoke)* | RFC_SYSTEM_INFO + CVERS query. Step 6.2 calls this to capture `server_release_marker`, `software_components`. |
| `sap-dev-core/shared/tables/sap_release_markers.tsv` | *(none — read by sap_rfc_system_info.ps1)* | (component, release range) → canonical marker lookup. |
| `<SKILL_DIR>/sap_login_select.ps1` | *(none — direct invoke)* | **Selection driver**. Actions: `init`, `decide`, `list`, `set-default`, `switch`, `delete`, `finalize`, `check`, `landscape-entries`. Emits structured signals (`RESOLVED:`, `ATTACH_ACTIVE:`, `CONNECT_PROFILE:`, `PICK_NEEDED:`, `ADD_NEEDED:`, `SUCCESS:`, `AMBIGUOUS:`, `CONTINUE_TO_STEP1:`, `LANDSCAPE:`). |
| `<SKILL_DIR>/references/sap_login_capture_active_session.vbs` | *(none — static)* | GUI-side capture. Phase-4 fields: `system_name`, `client`, `user`, `language`, `application_server`, `system_number`, `message_server`, `logon_group`, `program`, `screen_number`, plus GUI version. Emits flat JSON or `MULTI:<array>`. |
---
## Step 0 — Resolve Work Directory
Read sap-dev-core's settings.json (go 2 levels up from `<SKILL_DIR>` to the plugin root, then `settings.json`). Read `work_dir`, `custom_url`.
| Setting | Default if blank |
|---|---|
| `work_dir` | `C:\sap_dev_work` |
| `custom_url` | `{work_dir}\custom` |
Set `{WORK_TEMP}` = `{work_dir}\temp`
Ensure the temp directory exists:
```bash
cmd /c if not exist "{WORK_TEMP}" mkdir "{WORK_TEMP}"
```
---
## Argument Modes (Phase 4)
Branch on `$ARGUMENTS` before running anything below. Each mode is a single
PowerShell call to `sap_login_select.ps1`; the rest of this skill (Steps 1–6)
runs only for the **default mode** (connect-and-pin).
### Hint forms for `<ref>` (Phase 4.4)
`--switch`, `--set-default`, and `--delete` all accept a hint instead of a
raw UUID. The matcher tries these in order — first hit wins:
| Hint form | Meaning | Example |
|---|---|---|
| `<UUID>` | exact UUID (preserves legacy callers) | `df91fed6-8913-…` |
| `last` | profile with most recent `last_used_at` | `--switch last` |
| `default` | the configured default target | `--switch default` |
| `<SID>` | `system_name` exact match | `--switch S4D` |
| `<SID>/<CLIENT>` | SID + client filter (disambiguates same-SID profiles) | `--switch S4D/200` |
| `<SID>/<CLIENT>/<USER>` | SID + client + user | `--switch S4D/200/CONSULTANT` |
| `<description>` | exact, then substring on description OR system_name | `--switch dev-box` |
The slash form is recognised when the hint has 1 or 2 `/` AND every segment
is alphanumeric. Descriptions containing `/` still match via the description
rules below.
On match: action proceeds with the resolved UUID.
On **no match**: stdout `ERROR: no profile matches '<hint>'. Run /sap-login --list.` + exit 1.
On **multiple matches**: stdout `AMBIGUOUS: <json>` + exit 2. The JSON has an
`options[]` array of `{profile_id, description, system_name, client, user,
endpoint_summary}`. Run `AskUserQuestion`, then re-invoke with `-ProfileId <chosen-UUID>`.
### Modes
| Mode | Trigger phrase / arg | What runs |
|---|---|---|
| **list** | `--list`, "list connections", "show profiles" | `sap_login_select.ps1 -Action list` → emit `LIST: <json>`. Present a readable table to the user. **Done.** |
| **set-default** | `--set-default <ref>` | `sap_login_select.ps1 -Action set-default -ProfileId <ref>` → resolves via hint matcher → emit `SUCCESS: default_target_id=...`. **Done.** |
| **switch** | `--switch <ref>`, "switch to X" | `sap_login_select.ps1 -Action switch -ProfileId <ref>` → resolves via hint matcher → re-pins AI session, bumps `last_used_at`, releases old claims. Emits `CONTINUE_TO_STEP1:` and the skill **falls through to Step 1** (connect to the new system). Pass `-NoConnect` (or trigger `--switch <ref> --no-connect`) to re-pin only — `SUCCESS:` line appears without `CONTINUE_TO_STEP1:` and the skill is **Done.** |
| **delete** | `--delete <ref>` | Ask user to confirm deletion (`AskUserQuestion`); on confirm, `sap_login_select.ps1 -Action delete -ProfileId <ref>`. **Done.** |
| **check** | `--check`, "health-check connections", "doctor" | `sap_login_select.ps1 -Action check` → emit a per-profile health table covering DPAPI, DNS, RFC, and live GUI session. Read-only. **Done.** |
| **add** | `--add` | Skip Step 2 (decide); jump to ADD_NEEDED handler in Step 2. Treat as user-initiated new-profile entry. |
| **(default)** | no flag / no relevant arg | Run Steps 0.5 → 6.5 as documented below. |
---
## Step 0.5 — Start Logging
Start a structured log run. The helper persists `run_id` in a state file
(`{WORK_TEMP}\sap_login_run.json`) so subsequent steps and the final
log-end call append to the same run. Best-effort: silently no-ops if
`userConfig.log_enabled=false` or the lib can't load. **Do not** include
the SAP password in `-ParamsJson` — only system / client / user.
```bash
powershell -ExecutionPolicy Bypass -File "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_log_helper.ps1" -Action start -StateFile "{WORK_TEMP}\sap_login_run.json" -Skill sap-login -ParamsJson "{\"system\":\"<SID>\",\"client\":\"<CLIENT>\",\"user\":\"<USER>\"}"
```
---
## Step 0.7 — Bootstrap (AI session + Migration)
Resolves this conversation's AI-session id (automatic — see "AI-Session
Identity" section near the end of this file) and creates the per-PID
file at `{work_dir}\runtime\ai_session_by_pid\<owner_pid>.txt` if needed.
This step also performs a one-shot migration of the legacy single-connection
fields in `settings.json` into the new `connections.json` store (marking
them as the default). Idempotent.
```bash
powershell -ExecutionPolicy Bypass -File "<SKILL_DIR>\sap_login_select.ps1" -Action init -WorkTemp "{WORK_TEMP}"
```
Stdout signals:
- `INFO: ai_session_id=...` → the conversation's id; broker auto-resolves the same value on subsequent calls.
- `INFO: migrated legacy connection id=...` (optional) → notify the user that the previous single-connection settings were imported as the default.
- `SUCCESS: init complete` → proceed.
---
## Step 0.8 — Selection (decide)
Run the selection driver to pick the target connection. Re-invoke with the
appropriate `-PickProfileId` / `-PickConnectionPath` after each user choice.
```bash
powershell -ExecutionPolicy Bypass -File "<SKILL_DIR>\sap_login_select.ps1" -Action decide -WorkTemp "{WORK_TEMP}"
```
Interpret the **last** structured stdout line:
| Signal | Meaning | Action |
|---|---|---|
| `RESOLVED: path=<P> connection_id=<I> description='<D>'` | Existing AI-session pin already matches a live session. | Skip Steps 1–5; jump to Step 6 (capture) with this `path` and Step 6.5 with this `connection_id`. |
| `ATTACH_ACTIVE: path=<P> connection_id=<I> description='<D>'` | Pick is an already-open SAP connection. | Skip Steps 1–5; jump to Step 6 with this `path`. |
| `CONNECT_PROFILE: id=<I> description='<D>' source=<S>` | Pick is a saved profile. | Look up the profile in `connections.json` (`sap_connection_lib.ps1`), decrypt password (DPAPI), and proceed to Step 3 (Generate Login VBS) with the profile's fields. |
| `PICK_NEEDED: <json>` | User must choose. | Parse `options[]` (each has `kind=active|profile`, `description`, `system_name`, `client`, `user`, `endpoint_summary`, `is_default`). Present via `AskUserQuestion`. Re-invoke `sap_login_select.ps1 -Action decide -PickProfileId <id>` OR `-PickConnectionPath <path>`. |
| `ADD_NEEDED:` | No active connections, no saved profiles. | Prompt the user for a new connection (logon-pad entry, OR app server + system number, OR message server + logon group + system id) plus client/user/password/language. Proceed to Step 3 with the supplied values. After login succeeds, Step 6.5 saves it as a new profile. |
---
## Step 1 — Check Existing Session
Run the static check script directly — no token replacement or generation needed:
```bash
cscript //NoLogo "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_check_gui_login_status.vbs"
```
Replace `<SAP_DEV_CORE_SHARED_DIR>` with the absolute path to sap-dev-core's `shared/` directory.
Resolve it by going 3 levels up from `<SKILL_DIR>` (skill → skills/ → plugin dir → plugins root),
then into `sap-dev-core\shared`.
**Parse output:**
| STATUS line | Meaning | Action |
|---|---|---|
| `STATUS: LOGGED_IN` | Authenticated session found | Report session info (SYSTEM, CLIENT, USER, LANGUAGE, CODEPAGE from output). **Done — skip Steps 2-5.** |
| `STATUS: LOGIN_SCREEN` | Connection exists, needs authentication | Proceed to Step 2. The login VBS will reuse this session. |
| `STATUS: NO_SESSION` | SAP GUI running, no sessions | Proceed to Step 2 |
| `STATUS: NO_GUI` | SAP GUI / SAP Logon not running | Proceed to Step 2. The login VBS will start SAP Logon. |
| `STATUS: NO_SCRIPTING` | Scripting engine unavailable | Tell user to enable scripting: SAP Logon > Options > Scripting > Enable Scripting |
---
## Step 2 — Resolve Connection Parameters from the Pick
Only reached if Step 0.8 emitted `CONNECT_PROFILE`, `ADD_NEEDED`, or the user
needs credentials beyond what's stored. **`ATTACH_ACTIVE` and `RESOLVED` skip
this step entirely.**
### Step 2a — For `CONNECT_PROFILE: id=<I>`: load + decrypt
Dot-source the connection library and look up the profile:
```bash
powershell -ExecutionPolicy Bypass -Command @"
. '<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_connection_lib.ps1';
$p = Find-SapConnectionById -Id '<I>';
$p | ConvertTo-Json -Depth 4
"@
```
Use the returned fields directly as the `THE_*` substitutions in Step 3.
For `password_dpapi`, decrypt via `sap_dpapi.ps1`:
```bash
powershell -ExecutionPolicy Bypass -File "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_dpapi.ps1" -Action unprotect -Value "<password_dpapi-value>"
```
Stdout = plaintext password (or the same value pass-through with a stderr
warning for legacy plaintext). Exit code 1 = decrypt failed (different
Windows user or machine) → prompt the user for the password fresh and
remember to re-encrypt + save it in Step 6.5.
### Step 2b — For `ADD_NEEDED:` or `--add`: collect new credentials
**Step 2b-pre — Check the SAP Logon Pad landscape for known entries.**
Before asking the user to type endpoint values from scratch, enumerate the
entries they already have configured in SAP Logon (`SAPUILandscape.xml`,
`SAPUILandscapeGlobal.xml`, and legacy `saplogon.ini`):
```bash
powershell -ExecutionPolicy Bypass -File "<SKILL_DIR>\sap_login_select.ps1" -Action landscape-entries -WorkTemp "{WORK_TEMP}"
```
Parse the **last** stdout line:
| Signal | Action |
|---|---|
| `LANDSCAPE: []` | No entries found. Skip to the manual flow below. |
| `LANDSCAPE: [<json>]` | Parse the array. Each element has `name`, `kind` (`direct`/`load_balanced`), `server`, `system_number`, `message_server`, `logon_group`, `system_id`, `description`, `source`. Present a picker via `AskUserQuestion` whose options are each entry (label = `<name> — <kind> — <endpoint summary>`) plus one extra "Manual entry…" option. **One question, multi-select=false.** |
On entry-pick: **use the SAP Logon Pad method** — set `THE_LOGON_DESC` to
the picked `name`, and use the picked `system_id` / `system_name` for the
profile's `system_name` field (so the new reuse-loop identity check from
Phase 4.4 works). Leave server / system_number / msrv / grp / sid token
fields empty in Step 3 — SAP GUI resolves them from the pad entry. Then
ask only for client / user / password / language. The user types four
fields instead of ten.
On "Manual entry": fall through to the manual prompt below.
**Step 2b-manual — Type endpoint values directly.**
Ask the user via `AskUserQuestion` for whichever endpoint set they want to
configure:
- **SAP Logon pad entry** (simplest) — just the entry-name string. Then ask
for client / user / password / language.
- **Direct connect** — app server hostname/IP, 2-digit system number, plus
client / user / password / language. **Remember the hostname the user
typed** — pass it as `-UserAppServerHint` to Step 6.5 finalize so the
resolver can replace any divergent value returned by `Info.ApplicationServer`.
- **Load-balanced** — message server, logon group (default `SPACE`),
3-letter SystemID (R3NAME), plus client / user / password / language.
Optionally ask for a "Logon description" label (free text, max 60 chars);
if blank, `sap_login_select.ps1 -Action finalize` auto-derives it as
`<msrv|asrv>_<sid>_<client>_<user>`.
Encrypt the password BEFORE handing it to Step 3 if the user wants it
saved — pre-encrypt then pass `dpapi:...` to finalize. Otherwise Step 6.5
can re-encrypt:
```bash
powershell -ExecutionPolicy Bypass -File "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_dpapi.ps1" -Action protect -Value "<plaintext>"
# stdout: dpapi:AQAAAN...
```
### Step 2c — Legacy reminder
The legacy single-connection keys (`sap_logon_description`, `sap_application_server`,
`sap_system_number`, `sap_client`, `sap_user`, `sap_password`, `sap_language`)
are kept in `settings.json` for back-compat ONLY. Step 0.7's `init` imports
them into `connections.json` once; subsequent runs ignore them. Do **not**
read or write these keys directly from this skill.
### Step 2-legacy-a — DPAPI background (informational)
The stored `sap_password` value is one of three forms:
| Form | Example | Action |
|---|---|---|
| Empty | `""` | Skill will ask the operator to type it interactively. |
| DPAPI-encrypted | `dpapi:AQAAAN...` | Decrypt via `sap_dpapi.ps1` before injecting into the login VBS. |
| Plaintext (legacy) | `MyP@ssw0rd` | Pass through, but emit a one-line warning prompting re-save. |
Decrypt with:
```bash
powershell -ExecutionPolicy Bypass -File "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_dpapi.ps1" -Action unprotect -Value "<stored-value>"
```
Stdout = the plaintext password (or pass-through). Stderr = the
re-save warning (only on plaintext). Exit code 0 on success, 1 on
decrypt failure (wrong Windows user / different machine / corrupted
ciphertext) — in that case, prompt the operator to type the password
fresh and offer to encrypt-and-save it after Step 3 succeeds.
The plaintext password lives only in memory of the calling skill and
in the generated `{WORK_TEMP}\sap_login_run.vbs` (which Step 5
deletes). Do NOT echo the decrypted value to your reply or to any
log.
---
## Step 3 — Generate and Run the Login VBScript
The VBScript template is at `sap-dev-core/shared/scripts/sap_login.vbs`.
Write `{WORK_TEMP}\sap_login_run.ps1`:
```powershell
$content = [System.IO.File]::ReadAllText('<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_login.vbs', [System.Text.Encoding]::UTF8)
$content = $content.Replace('%%SAP_LOGON_DESCRIPTION%%','THE_LOGON_DESC')
$content = $content.Replace('%%SAP_APPLICATION_SERVER%%','THE_SERVER')
$content = $content.Replace('%%SAP_SYSTEM_NUMBER%%','THE_SYSNR')
$content = $content.Replace('%%SAP_MESSAGE_SERVER%%','THE_MSG_SERVER')
$content = $content.Replace('%%SAP_LOGON_GROUP%%','THE_LOGON_GROUP')
$content = $content.Replace('%%SAP_SYSTEM_ID%%','THE_SYSTEM_ID')
$content = $content.Replace('%%SAP_SYSTEM_NAME%%','THE_SYSTEM_NAME')
$content = $content.Replace('%%SAP_CLIENT%%','THE_CLIENT')
$content = $content.Replace('%%SAP_USER%%','THE_USER')
$content = $content.Replace('%%SAP_PASSWORD%%','THE_PASSWORD')
$content = $content.Replace('%%SAP_LANGUAGE%%','THE_LANGUAGE')
[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_login_run.vbs', $content, [System.Text.Encoding]::Unicode)
Write-Host 'Done'
```
Replace all `THE_*` placeholders with actual values from Step 2.
`THE_PASSWORD` is the **decrypted plaintext** from Step 2a (NOT the
`dpapi:` ciphertext — DPAPI is a storage-at-rest concern; SAP GUI
needs plaintext). Replace `<SAP_DEV_CORE_SHARED_DIR>` with the
resolved path.
Token-fill rules (the VBS picks among three connection methods):
- For **SAP Logon pad** profiles: set `THE_LOGON_DESC` to the entry name; leave server/sysnr/msrv/grp/sid empty.
- For **direct** profiles: set `THE_SERVER` + `THE_SYSNR`; leave logon_desc/msrv/grp/sid empty.
- For **load-balanced** profiles: set `THE_MSG_SERVER` + `THE_LOGON_GROUP` (blank → VBS uses " ") + `THE_SYSTEM_ID`; leave logon_desc/server/sysnr empty.
- `THE_SYSTEM_NAME` is **always** the profile's `system_name` (SID — e.g. `S4D`, `S4H`). Used by the VBS reuse-loop's identity match — independent of which of the three endpoint methods is chosen. Leave empty for legacy profiles with no SID; the VBS falls back to Client+User matching.
If a field is unused in the chosen method, substitute the empty string `""`.
Run:
```bash
powershell -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_login_run.ps1"
```
### Execute
```bash
cscript //NoLogo {WORK_TEMP}\sap_login_run.vbs
```
**The VBScript handles four scenarios** (first match wins):
1. **Login-screen session found** → Reuses existing connection, fills credentials.
2. **SAP Logon Description provided** → Opens connection via `OpenConnection(desc)`, fills login screen.
3. **MessageServer provided + ApplicationServer empty** → Opens load-balanced via `OpenConnectionByConnectionString("/M/<msrv>/G/<grp>/S/<sid>")` (LogonGroup defaults to one space when blank).
4. **ApplicationServer provided** → Opens direct via `OpenConnectionByConnectionString("/H/<server>/S/<port>")` where port = 3200 + SystemNumber.
**On success** (last line starts with `SUCCESS:`): tell the user login succeeded. Show full output.
**On failure** (any line starts with `ERROR:`): show full output and diagnose:
| Error | Cause | Fix |
|---|---|---|
| `SAP GUI is not running` | SAP Logon not open | Start SAP Logon |
| `Could not get SAP Scripting Engine` | Scripting disabled | SAP Logon > Options > Scripting > Enable |
| `Could not open connection` | Wrong entry name | Check exact name in SAP Logon pad |
| `Load-balanced login requires SAP_SYSTEM_ID` | Missing R3NAME on load-balanced profile | Set `system_id` on the profile (3-letter SID) |
| `No endpoint configured` | All three endpoint sets blank | Configure profile with one of: logon_pad_entry, message_server + system_id, or application_server + system_number |
| `Could not open connection with string` | Server unreachable or wrong server/port | Check server hostname and system number |
| `Could not open load-balanced connection` | Message server unreachable / wrong logon group / wrong SID | Check msrv hostname, logon group exists in RZ12, SID matches /sapmnt/<SID> |
| `Login failed` | Wrong credentials | Check client, username, and password in profile |
| `Login timed out` | No manual login within 5 min | Re-run and log in promptly |
---
## Step 4 — Verify RFC Connectivity (Optional)
**Run this step only when the user explicitly requests RFC verification**, or when
the calling skill needs RFC (e.g., sap-check-fm, sap-fix-fm, sap-check-abap).
The RFC connection template is at `sap-dev-core/shared/scripts/sap_rfc_connect.ps1`.
For load-balanced profiles, call `Connect-SapRfc` directly with the
`-MessageServer / -LogonGroup / -SystemID` parameter set rather than the
template (the template is direct-server only). Either path works for
verification — pick the one matching the profile's endpoint shape.
Direct-server path (write `{WORK_TEMP}\sap_rfc_test_run.ps1`):
```powershell
$content = [System.IO.File]::ReadAllText('<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_rfc_connect.ps1', [System.Text.Encoding]::UTF8)
$content = $content.Replace('%%SAP_APPLICATION_SERVER%%','THE_SERVER')
$content = $content.Replace('%%SAP_SYSTEM_NUMBER%%','THE_SYSNR')
$content = $content.Replace('%%SAP_CLIENT%%','THE_CLIENT')
$content = $content.Replace('%%SAP_USER%%','THE_USER')
$content = $content.Replace('%%SAP_PASSWORD%%','THE_PASSWORD')
$content = $content.Replace('%%SAP_LANGUAGE%%','THE_LANGUAGE')
$content = $content.Replace('%%RFC_LIB_PS1%%','<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_rfc_lib.ps1')
[System.IO.File]::WriteAllText('{WORK_TEMP}\sap_rfc_test_run.ps1', $content, [System.Text.Encoding]::UTF8)
Write-Host 'Done'
```
Replace all `THE_*` placeholders.
Load-balanced path (inline):
```powershell
. "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_rfc_lib.ps1"
$dest = Connect-SapRfc `
-MessageServer THE_MSG_SERVER `
-LogonGroup THE_LOGON_GROUP `
-SystemID THE_SYSTEM_ID `
-Client THE_CLIENT `
-User THE_USER `
-Password THE_PASSWORD `
-Language THE_LANGUAGE
if ($dest) { Write-Host 'RFC_OK' } else { Write-Host 'ERROR: RFC connect failed' }
```
Execute via **32-bit PowerShell** (SAP NCo 3.1 is registered in the 32-bit GAC):
```bash
C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File "{WORK_TEMP}\sap_rfc_test_run.ps1"
```
**On success** (output contains `RFC_OK`): tell user RFC connection verified.
**On failure** (output contains `ERROR:`): show full output and diagnose:
| Error | Cause | Fix |
|---|---|---|
| `NCo 3.1 not found in GAC_32` | SAP NCo 3.1 not installed for .NET 4.0 32-bit | Install SAP NCo 3.1 for .NET 4.0 (32-bit) per SAP Note |
| `RFC logon failed` | Wrong server/credentials or server unreachable | Check all connection parameters |
**RFC connection logic:**
- The PS1 template uses NCo `RfcDestinationManager` with the supplied parameters and pings the destination to verify connectivity.
---
## Step 5 — Clean Up
```bash
cmd /c del {WORK_TEMP}\sap_login_run.vbs & del {WORK_TEMP}\sap_login_run.ps1 & del {WORK_TEMP}\sap_rfc_test_run.ps1
```
---
## Step 5b — Optionally Encrypt and Save the Password (post-login, opt-in)
**Run only when at least one of these is true:**
- The password was just typed by the operator this turn (i.e. not
read from `settings.json`).
- The stored `sap_password` is plaintext (Step 2a emitted the
pass-through warning) and the operator wants to upgrade it.
- The stored value was DPAPI-encrypted but on a different user /
machine, so Step 2a's decrypt failed, and the operator just typed
the correct password.
**Ask the operator explicitly** before persisting — never auto-save:
> Save the password to `settings.json` (DPAPI-encrypted, bound to
> your Windows user account)? **yes** to save, anything else to skip.
If the operator says yes, encrypt and persist:
```bash
# 1. Encrypt the plaintext via the shared DPAPI helper.
powershell -ExecutionPolicy Bypass -File "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_dpapi.ps1" -Action protect -Value "<plaintext>"
# → stdout: dpapi:AQAAAN...
```
Capture the `dpapi:` line and write it to `settings.json` via
`/update-config` (or by direct edit of the `userConfig.sap_password.value`
field). Both produce the same on-disk result; `/update-config` is the
preferred path because it preserves JSON formatting:
```
/update-config userConfig.sap_password = "dpapi:AQAAAN..."
```
**DO NOT** echo the plaintext back to the user, and do not write the
plaintext to any log. The shared `sap_log_lib.ps1` already redacts
`sap_password` by key name; with DPAPI in place, the value at rest is
no longer the secret either.
### What happens if I copy `settings.json` to another machine?
The `dpapi:` ciphertext is bound to the **Windows user account on
this machine**. On another machine (or another Windows account), the
decrypt step in 2a will fail with `sap_dpapi: decrypt failed (wrong
Windows user / different machine / corrupted ciphertext)`. The skill
will then prompt for the password fresh, and the operator can re-save
on that new machine. This is the desired property: a leaked
`settings.json` is useless without the matching Windows profile.
---
## Step 6 — Active-Session Capture
Reached after `sap_login.vbs` succeeded **or** when Step 0.8 produced
`ATTACH_ACTIVE` / `RESOLVED` (the session already exists). The capture VBS
reads the **rich Phase-4 GuiSessionInfo set** — system / client / user /
language, MessageServer / Group / SystemNumber / ApplicationServer,
Program / ScreenNumber, plus GUI version — and emits a single-line JSON
record.
```bash
C:/Windows/SysWOW64/cscript.exe //NoLogo "<SKILL_DIR>\references\sap_login_capture_active_session.vbs" "<session-path-from-step-0.8-or-blank>"
```
Last line of stdout:
- `{"session_path":"/app/con[0]/ses[0]", ...}` — single-line JSON record. Save this to a variable for Step 6.5.
- `MULTI:[ {...}, ... ]` — only fires when the wrapper passed no hint AND multiple connections are attached AND Step 0.8 didn't already pick one. Should be rare after Phase 4 (Step 0.8 picks first). If it happens, present a picker via `AskUserQuestion`, then re-invoke the VBS with **both** the chosen `session_path` **and** the `connection_string` field from the chosen entry as a second argument. SAP GUI can reorder connection indices between the two calls; the second argument lets the VBS recover by scanning for the connection by description when the path-based lookup returns the wrong session:
```bash
C:/Windows/SysWOW64/cscript.exe //NoLogo "<SKILL_DIR>\references\sap_login_capture_active_session.vbs" "<chosen-session_path>" "<chosen-connection_string>"
```
- `WARN: SAP GUI reordered connections ...` — logged when the path hint matched a different connection than expected; the VBS scanned by description and recovered automatically.
- `ERROR: <text>` — skip Step 6.5 and warn; downstream skills still work via the broker's discovery path, just without an explicit pin.
### Step 6.2 — Optional RFC system info (deferred)
The legacy `sap_rfc_system_info.ps1` enrichment is no longer driven from
this skill by default. Run it on demand when a consumer skill needs
`server_release_marker` / `software_components`. See
`<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_rfc_system_info.ps1` for the call
convention.
---
## Step 6.5 — Finalize (save profile + pin AI session)
Hand the captured JSON to `sap_login_select.ps1 -Action finalize`. This is
the step that:
1. **Reconciles `application_server`** via `Resolve-SapApplicationServer`. SAP GUI's `Info.ApplicationServer` returns the host's *internal* identity (e.g. `s4sapdev`), which is not DNS-resolvable from NAT / dynamic-DNS / reverse-proxy workstations. The resolver runs a three-step cascade: captured value → user hint (`-UserAppServerHint`) → SAP Logon Pad entry's `SAPUILandscape.xml` / `saplogon.ini` lookup. First DNS hit wins; on total failure the captured value is kept and a `WARN:` is emitted (SAP GUI keeps working; RFC will be the casualty).
2. Saves (or merges via 4-step dedup) the profile in `{work_dir}\runtime\connections.json`.
3. Assigns the profile's UUID as `connection_id` on the live broker registry block (`broker set-connection-id`).
4. Pins the AI session to that `connection_id` (`broker pin`). On a switch, this releases stale claims from the old connection.
5. Phase 4.2: the pin file `sap_active_session.json` is **removed entirely**. Consumer skills get the session path via `Get-SapCurrentSessionPath` (from `sap_connection_lib.ps1`) — which reads the broker's `session_registry.json` for the AI-session's pinned connection, finds a usable session there, and returns the path. Version info goes through `Get-SapCurrentConnectionProfile` (reads the profile in `connections.json` by `connection_id`).
```powershell
$captured = '<single-line JSON from Step 6>' # exact string
$newDesc = '<user-supplied Logon description, or empty>'
$newPwd = '<dpapi:... ciphertext, or empty>'
$userHost = '<hostname the user typed in Step 2b ADD flow, or empty>'
powershell -ExecutionPolicy Bypass -File "<SKILL_DIR>\sap_login_select.ps1" `
-Action finalize `
-WorkTemp "{WORK_TEMP}" `
-CapturedJson $captured `
-NewLogonDescription $newDesc `
-NewPasswordDpapi $newPwd `
-UserAppServerHint $userHost
```
Pass `-UserAppServerHint` when the user typed the application-server hostname directly in Step 2b's ADD flow — it takes precedence over the SAPUILandscape lookup. For SAP-Logon-Pad-only flows (existing entry), leave it empty and the resolver auto-discovers from saplogon.
Expected stdout (one of):
- `INFO: application_server='<host>' (captured; DNS-resolvable)` — happy path; no rewrite.
- `INFO: application_server='<host>' (user hint resolves; replacing captured '<x>')`
- `INFO: application_server='<host>' (resolved via SAP Logon Pad entry '<entry>'; replacing captured '<x>')`
- `WARN: application_server='<x>' is NOT DNS-resolvable from this workstation. SAP GUI will work; RFC will not until you correct this value (edit connections.json or rerun /sap-login with -UserAppServerHint <hostname>).`
Then:
- `INFO: profile saved id=<UUID> description='<auto-derived or user-supplied>'`
- `INFO: ai_session=... pinned to connection_id=...` (no pin file in Phase 4.2)
- `SUCCESS: connection_id=<UUID> description='<...>' session_path=/app/con[N]/ses[M]`
If the `WARN:` fires AND the user expects RFC to work, **prompt for the correct hostname via `AskUserQuestion`** and rerun finalize with `-UserAppServerHint <answer>`. **Do NOT block the GUI flow** — RFC is optional for most SAP work; the profile is still saved with the captured value as a placeholder.
Tell the user the connection is ready, including the description and
whether it became the new default.
### Step 6.6 — Switch-mode follow-up
If the user invoked `/sap-login --switch <id>`, the `switch` action
already re-pinned during Argument-Mode dispatch. Now Step 0.8's `decide`
runs, will either ATTACH_ACTIVE (target already open) or CONNECT_PROFILE
(open a new SAP GUI connection). Step 6.5 finalize re-affirms the pin.
The broker's `pin` action already released any claims this AI session
held on the OLD connection — those SAP sessions are now back at Easy
Access and free for the user or another AI session.
---
## Consumer-skill resolution contract (unchanged interface, Phase-4 enriched)
Every downstream skill resolves its target session like this:
1. Explicit `--session "<path>"` arg → use it.
2. Else use `Get-SapCurrentSessionPath` (in `sap_connection_lib.ps1`): looks up `session_registry.json`'s `ai_sessions[<id>].connection_id` for this AI session, finds the matching connection block, and returns a usable session path on it. Falls back to sole-connection auto-default for the single-conn case.
3. Else exactly one connection attached → silent default `/app/con[0]/ses[0]`.
4. Else refuse with: *"multiple SAP GUI connections detected and no active session pinned; run `/sap-login` first to pick one."*
The broker (sap_session_broker.ps1) ALSO enforces AI-session pin: an
`acquire` for an AI session that's pinned to connection A will be refused
if it targets connection B. This is the safety net that prevents subagents
from "drifting" to a different SAP system.
---
## Final — Log End
Log the run-end record. Best-effort: silently no-ops if logging disabled.
On success:
```bash
powershell -ExecutionPolicy Bypass -File "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_login_run.json" -Status SUCCESS -ExitCode 0
```
On failure (substitute `<CLASS>` and short message):
```bash
powershell -ExecutionPolicy Bypass -File "<SAP_DEV_CORE_SHARED_DIR>\scripts\sap_log_helper.ps1" -Action end -StateFile "{WORK_TEMP}\sap_login_run.json" -Status FAILED -ExitCode 1 -ErrorClass <CLASS> -ErrorMsg "<short>"
```
Suggested `<CLASS>`: `LOGIN_FAILED`, `RFC_LOGON_FAILED`, `GUI_TIMEOUT`.
---
## AI-Session Identity (automatic, parent-PID-based)
The Phase-4 AI-session pin scope is identified by an id derived from
the **parent-process PID** of the running skill. `Get-SapAiSessionId` in
`sap_connection_lib.ps1` walks up the process tree from the current
PowerShell/cscript, skipping script-host processes (`powershell`, `pwsh`,
`cscript`, `wscript`, `cmd`, `conhost`), and stops at the first
non-script-host ancestor — that's the Claude Code conversation process.
Subagents launched from the same conversation share that ancestor and
therefore share the id; parallel conversations have different parent
PIDs and therefore get different ids.
State lives at `{work_dir}\runtime\ai_session_by_pid\<owner_pid>.txt`
(one file per conversation). Opportunistic GC drops files for PIDs that
are no longer alive, so the directory stays small.
**No SessionStart hook needed.** Earlier drafts of Phase 4 used a write-
once-if-missing `ai_session_id.txt` file written by a hook, but that
silently shared one id across parallel conversations. The parent-PID
mechanism is automatic, scoped correctly, and has no external setup
requirement.
**Override:** the env var `SAPDEV_AI_SESSION_ID`, if set non-empty, takes
precedence over the walked id. Tests and manual one-offs use it; normal
operation doesn't.
---
## Security Note
**At rest** — `sap_password` in `settings.json` is stored DPAPI-encrypted
(`dpapi:<base64>`) under the CurrentUser scope. Decryption is bound to
the Windows user account on the machine that performed the encryption;
a copied `settings.json` is useless on another machine. See Step 2a
(decrypt-on-read) and Step 5b (encrypt-on-save). Plaintext values are
still accepted for backward compatibility but trigger a warning that
prompts the operator to re-save.
**In flight** — `{WORK_TEMP}\sap_login_run.vbs` and
`{WORK_TEMP}\sap_rfc_test_run.ps1` contain the **decrypted plaintext**
during execution because SAP GUI / NCo need it that way. Step 5
deletes both files immediately after use. Never re-use these files
across runs and never copy them out of `{WORK_TEMP}`.
**In logs** — `sap_log_lib.ps1` / `sap_log_lib.vbs` redact `sap_password`
by key name (`log_redact_keys` setting). With DPAPI on top, the
on-disk `value` is also no longer the real secret.
The check script (`sap_check_gui_login_status.vbs`) never touches
credentials. The `sensitive: true` flag on `sap_password` masks the
value in the Claude Code UI display; it is independent of the DPAPI
storage layer.
---
## 32-bit Note
SAP GUI 7.70 and older is exclusively 32-bit. SAP NCo 3.1 is registered in the
32-bit GAC (`C:\Windows\Microsoft.NET\assembly\GAC_32`) when installed for .NET 4.0 32-bit.
- **Check script** (`sap_check_gui_login_status.vbs`): runs with standard `cscript`.
- **SAP GUI login VBS** (`sap_login.vbs`): runs with standard `cscript` (SAP GUI
Scripting works with both 32-bit and 64-bit).
- **RFC connection PS1** (`sap_rfc_connect.ps1`): **must** use 32-bit PowerShell:
`C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe`.
---
## Important: Encoding
When filling VBS templates, always write with **Unicode** (UTF-16 LE) encoding.
Use `[System.Text.Encoding]::Unicode` in PowerShell.
UTF-16 LE is what `cscript` supports natively and preserves non-ASCII characters.
UTF-8 with BOM causes a cscript compile error.
The check script is committed as UTF-8 and run directly — cscript handles ASCII-only VBS fine.
No comments yet. Be the first to comment!