sap-ai-dev-toolkit 0.9.9923 β 0.9.9924
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +2 -1
- package/README.md +27 -5
- package/dist/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/package.json +1 -1
- package/patches/vsp-single-logon.patch +258 -0
- package/scripts/build-vsp.mjs +1 -1
package/NOTICE
CHANGED
|
@@ -3,7 +3,8 @@ Third-party notices for sap-ai-dev-toolkit
|
|
|
3
3
|
This package includes patched VSP (vibing-steampunk) binaries built from upstream commit
|
|
4
4
|
9886d2727f47506368b0a3c2f1c1766f1200f747. The BAS proxy-auth change is documented in
|
|
5
5
|
patches/vsp-bas-proxy-auth.patch; the namespaced class-include EditSource fix is documented in
|
|
6
|
-
patches/vsp-namespaced-class-include-edit.patch
|
|
6
|
+
patches/vsp-namespaced-class-include-edit.patch; the shared-logon fix for concurrent ADT requests is
|
|
7
|
+
documented in patches/vsp-single-logon.patch. The following upstream notice is reproduced from VSP:
|
|
7
8
|
|
|
8
9
|
vibing-steampunk (vsp)
|
|
9
10
|
Copyright (c) 2025-2026 Alice Vinogradova and contributors
|
package/README.md
CHANGED
|
@@ -75,6 +75,16 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
|
|
|
75
75
|
|
|
76
76
|
**Prerequisite:** Node.js 20 or newer. The pinned runtime ships with the package; no Go toolchain is installed or required.
|
|
77
77
|
|
|
78
|
+
## π What's new
|
|
79
|
+
|
|
80
|
+
| Version | Change |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| 0.9.9924 | **One SAP logon per runtime, shared by parallel tool calls.** AI assistants fire many tool calls at once; each used to reach SAP before the first security-session cookie came back, so every call logged on again and SM04 / the SAP GUI "already logged on" list filled with dozens of sessions. Now the first request logs on, the parallel ones wait for it and reuse its `SAP_SESSIONID` cookie (Basic, SSO and local SAP GUI entries alike); after an expired session the next burst again shares one fresh logon. |
|
|
83
|
+
| 0.9.9923 | **Oh My Pi (OMP) receives all twelve agents** as task agents (`~/.omp/agent/agents/<id>.md`, dispatched by kebab-case id) next to the twenty-two skills. **Four new companion MCP servers:** SAP Datasphere (`sap-ai-datasphere`, incl. BW β Datasphere migration), SAP Build Process Automation (`sap-ai-process-automation`, all eight BPA APIs), SAP BTP platform administration (`sap-ai-btp`, on your own `btp login --sso`), and SAP Cloud ALM (`sap-ai-cloud-alm`), each with its own agent, skills and `--setup-*` wizard. |
|
|
84
|
+
| 0.9.9922 | `EditSource` no longer double-encodes namespaced class names (`/NS/CL_β¦`) on class-include writes. |
|
|
85
|
+
| 0.9.9921 | Simpler Integration Suite setup wizard: shorter intro, clearer status lines, and a single credential menu. |
|
|
86
|
+
| 0.9.9920 | **Integration Suite companion** (`sap-ai-integration`, since 0.9.9914) expanded across the lifecycle: API Management content, documentation, runtime and monitoring reads, confirmation-gated cleanup and delete tools, and asynchronous Cloud Integration monitoring. |
|
|
87
|
+
|
|
78
88
|
## π€ Available agents and skills
|
|
79
89
|
|
|
80
90
|
**Agents:** Twelve user-invocable custom agents are included. Select the best fit from the agent picker in your harness's chat (GitHub Copilot, Claude Code, or opencode), dispatch the matching OMP task agent by its kebab-case id, or use the matching Pi prompt command, as shown in Quick start. Harnesses without custom agents (OpenAI Codex, Cursor, Gemini CLI, Cline, Roo Code, Windsurf / Devin Desktop) get the same workflows as skills, for example `sap-integration-architect` and `integration-suite-delivery`.
|
|
@@ -100,11 +110,11 @@ Agents and skills install for the harnesses you pick during global installation:
|
|
|
100
110
|
|
|
101
111
|
## β¨ Your SAP development cockpit, inside your AI assistant
|
|
102
112
|
|
|
103
|
-
`sap-ai-dev-toolkit` installs `sap-ai-dev`, discovers your BAS destinations or local SAP GUI systems, and exposes a curated SAP development toolset through MCP. Instead of manually switching between chat, terminal commands, repository searches, ADT screens, and SAP checks, describe the outcome you want and let the appropriate bundled agent coordinate the available tools.
|
|
113
|
+
`sap-ai-dev-toolkit` installs `sap-ai-dev`, discovers your BAS destinations or local SAP GUI systems, and exposes a curated SAP development toolset through MCP. Optional companion servers from the same package reach the rest of the landscape: HANA Cloud, Integration Suite, Datasphere, Build Process Automation, the BTP platform, and Cloud ALM. Instead of manually switching between chat, terminal commands, repository searches, ADT screens, cockpits, and SAP checks, describe the outcome you want and let the appropriate bundled agent coordinate the available tools.
|
|
104
114
|
|
|
105
115
|
<p align="center"><strong>π¬ Request β π Inspect β π§ Reason β π§βπ» Implement β π§ͺ Verify β π Report</strong></p>
|
|
106
116
|
|
|
107
|
-
The workflow covers ABAP, CDS, RAP, repository analysis, table and query access, testing, ATC, application logs, transports, and controlled source changes. The live MCP tool list remains the source of truth for what your connected SAP system and active runtime mode expose.
|
|
117
|
+
The workflow covers ABAP, CDS, RAP, repository analysis, table and query access, testing, ATC, application logs, transports, and controlled source changes, plus integration, data, process and platform operations through the companions. The live MCP tool list remains the source of truth for what your connected SAP system and active runtime mode expose.
|
|
108
118
|
|
|
109
119
|
## π Feature highlights
|
|
110
120
|
|
|
@@ -116,6 +126,13 @@ The workflow covers ABAP, CDS, RAP, repository analysis, table and query access,
|
|
|
116
126
|
| π§© | **CDS + RAP development** | Explore models and dependencies, then build RAP business objects and services with system context. |
|
|
117
127
|
| ποΈ | **Ground decisions in SAP data** | Inspect DDIC structures, read table contents, and run controlled ABAP SQL queries. |
|
|
118
128
|
| βοΈ | **Read-only HANA Cloud inspection** | Inspect an environment-bound HDI schema, object metadata, and capped query results without exposing deployment credentials. |
|
|
129
|
+
| π | **Integration Suite lifecycle** | Build, release, deploy, monitor, diagnose and recover Cloud Integration and API Management content with retained releases and confirmation-gated writes. |
|
|
130
|
+
| ποΈ | **Datasphere and BW migration** | Model, deploy, load and administer Datasphere content; inventory BW models and reconcile BW against Datasphere after migration. |
|
|
131
|
+
| βοΈ | **Build Process Automation operations** | Inspect definitions and instances, unstick failed workflows, work user-task queues, test decisions, and trace business events across all eight BPA APIs. |
|
|
132
|
+
| ποΈ | **BTP platform administration** | Audit account hierarchy, entitlements and quotas, service instances and keys, destinations, trust and Cloud Connectors on your own `btp` CLI login. |
|
|
133
|
+
| π‘ | **Cloud ALM observability** | Find exception and alert hotspots per DEV/TEST/PROD tier and correlate them with live CPI message logs and SLG1. |
|
|
134
|
+
| π§© | **Agents in eleven AI harnesses** | Copilot, Claude Code, opencode and OMP get native agents, Pi gets prompt commands, and every harness gets the skills. |
|
|
135
|
+
| π | **One logon per runtime** | Parallel tool calls share a single SAP security session instead of opening one per call. |
|
|
119
136
|
| βοΈ | **Edit ABAP safely** | Update supported source, stage reviewed change sets, create transport requests, and run syntax checks before requested activation. |
|
|
120
137
|
| β
| **Quality built into the flow** | Pair local ABAP linting with SAP syntax checks, ABAP Unit, ATC, editor diagnostics, and formatting. |
|
|
121
138
|
| π | **Debug with system context** | Investigate dumps, traces, application logs, runtime failures, capabilities, and installed components. |
|
|
@@ -197,17 +214,17 @@ The package ships with twelve custom agents plus twenty-two task-focused Agent S
|
|
|
197
214
|
|
|
198
215
|
## πΊοΈ How it fits together
|
|
199
216
|
|
|
200
|
-
Each selected BAS destination becomes its own isolated MCP server identity. The MCP client discovers the live tools and schemas, the add-on routes calls to the correct runtime process, and
|
|
217
|
+
Each selected BAS destination or local SAP GUI system becomes its own isolated MCP server identity. The MCP client discovers the live tools and schemas, the add-on routes calls to the correct runtime process, and that process reaches SAP ADT through the selected route over one shared SAP logon. Companion servers (HANA, Integration Suite, Datasphere, Process Automation, BTP, Cloud ALM) are separate stdio MCP processes with their own user-only credentials, so an agent can combine them β for example a Cloud ALM alert, the matching CPI message log, and the ABAP SLG1 entry.
|
|
201
218
|
|
|
202
219
|
## π§° Optional full-stack companion MCP servers
|
|
203
220
|
|
|
204
|
-
ABAP/RAP backend access is provided by this add-on's
|
|
221
|
+
ABAP/RAP backend access is provided by this add-on's destination-scoped runtime. For end-to-end SAP development, setup can also add managed companion MCP entries for frontend, CAP, browser validation, read-only HANA inspection, Integration Suite, Datasphere, Build Process Automation, BTP platform administration, and Cloud ALM:
|
|
205
222
|
|
|
206
223
|
```sh
|
|
207
224
|
sap-ai-dev --setup --tools
|
|
208
225
|
```
|
|
209
226
|
|
|
210
|
-
The companion entries are optional and are launched through `npx` only when the MCP client starts them. They are marked as managed by `sap-ai-dev-toolkit`, so rerunning setup can update or remove them without touching unrelated MCP servers. The HANA, Integration Suite
|
|
227
|
+
The companion entries are optional and are launched through `npx` only when the MCP client starts them. They are marked as managed by `sap-ai-dev-toolkit`, so rerunning setup can update or remove them without touching unrelated MCP servers. The companions shipped in this package (HANA, Integration Suite, Datasphere, Process Automation, BTP, Cloud ALM) use `--ignore-scripts` so the toolkit postinstall wizard cannot run during MCP startup. Each has a dedicated wizard: `sap-ai-dev --setup-integration`, `--setup-datasphere`, `--setup-process-automation`, `--setup-btp`, and `--setup-cloud-alm`.
|
|
211
228
|
|
|
212
229
|
| MCP entry | Package | Launches | Use when the agent needs to |
|
|
213
230
|
| --- | --- | --- | --- |
|
|
@@ -219,6 +236,7 @@ The companion entries are optional and are launched through `npx` only when the
|
|
|
219
236
|
| `integration-suite` | `sap-ai-dev-toolkit` | `sap-ai-integration` | Build, deploy, undeploy, and monitor Cloud Integration content (iFlows, value mappings, script collections, security material, Partner Directory) and manage API Management providers, proxies, policies, products, and key value maps |
|
|
220
237
|
| `datasphere` | `sap-ai-dev-toolkit` | `sap-ai-datasphere` | Model and administer SAP Datasphere: spaces, users, roles, database users, connections, modeling objects and packages, task chains, replication flows, Data Marketplace, consumption queries and Open SQL |
|
|
221
238
|
| `process-automation` | `sap-ai-dev-toolkit` | `sap-ai-process-automation` | Inspect and operate SAP Build Process Automation across all eight BPA APIs: process definitions, workflow instances with contexts, logs and errors, stuck instances, user tasks and the user inbox, decisions, API triggers, automations, process visibility, administration jobs, process starts and business events |
|
|
239
|
+
| `sap-btp` | `sap-ai-dev-toolkit` | `sap-ai-btp` | Audit and operate the BTP core platform through your own `btp login --sso`: account hierarchy, entitlements and quotas, Service Manager instances and service keys, destinations, trust, and Cloud Connectors |
|
|
222
240
|
| `cloud-alm` | `sap-ai-dev-toolkit` | `sap-ai-cloud-alm` | Observe and triage SAP landscapes with SAP Cloud ALM: health, exception and alert hotspots per tier, Integration & Exception Monitoring records, monitoring events with CPI and SLG1 handoffs, analytics, feature and transport status, and confirmation-gated alert closing |
|
|
223
241
|
|
|
224
242
|
Recommended profiles:
|
|
@@ -1094,6 +1112,8 @@ If `GetSystemInfo` or `RunQuery` reports a CSRF-token fetch failure, the runtime
|
|
|
1094
1112
|
|
|
1095
1113
|
**`MCP config ... contains invalid JSON`** usually means one of three things, all healed automatically by setup: the file starts with a byte-order mark (Windows PowerShell `>` redirection, `Set-Content`, or Notepad's "UTF-8 with BOM" mode β invisible in editors and terminals), it contains comments or trailing commas (VS Code reads `mcp.json` as JSONC), or setup raced an editor save and read a torn file (retried once before healing). Unrecoverable content is backed up as `mcp.json.<kind>-<timestamp>.bak` and replaced; the backup path is printed in the setup warning. On older versions, re-save the file as UTF-8 without BOM (VS Code: "Change File Encoding β Save with Encoding β UTF-8") and rerun `sap-ai-dev --setup`.
|
|
1096
1114
|
|
|
1115
|
+
**SAP shows "You are already logged on to the system in the following sessions" with dozens of entries** (or SM04 fills with HTTP sessions of your user): runtimes before 0.9.9924 opened one SAP security session per parallel tool call until the first session cookie came back. Upgrade and restart the MCP server; each runtime process now logs on once and shares that session. Sessions left by older processes expire with the system's HTTP session timeout, or end them with **Cancel all existing logons** at the next SAP GUI logon. Every MCP server start (editor reload, `/mcp reload`, a second editor) is still one process with one logon of its own.
|
|
1116
|
+
|
|
1097
1117
|
If automatic runtime provisioning fails, check network access and the package's supported platform. You can install Go manually or set `GO_BINARY` as an explicit fallback. A trusted prebuilt executable can be supplied with `SAP_AI_DEV_TOOLKIT_BINARY`.
|
|
1098
1118
|
|
|
1099
1119
|
## π§βπ» Development
|
|
@@ -1106,6 +1126,8 @@ npm test
|
|
|
1106
1126
|
|
|
1107
1127
|
Install the checkout globally with `npm run install:local`, or use `npm run install:local -- --link` for a live workspace link. Verify without reinstalling using `npm run install:local -- --verify`: it checks the installed package version and, for a link, the actual workspace target, and refuses an outdated snapshot or a different checkout. After installation, exercise the installed executable with `sap-ai-integration --check`; npm-linked commands resolve their real entrypoint before starting the server.
|
|
1108
1128
|
|
|
1129
|
+
The ABAP runtime is upstream VSP pinned to one commit and patched with `patches/*.patch` (applied in the order listed in `scripts/build-vsp.mjs`). `npm run build:vsp` clones it into `.build/vibing-steampunk`, applies the patches, and builds `dist/`; run `go test ./pkg/adt/ ./internal/...` there to exercise a patch. To change a patch, commit the patched tree in `.build`, edit, and write `git diff` back to the patch file.
|
|
1130
|
+
|
|
1109
1131
|
The package repository is [grknylmz/sap-ai-dev-toolkit](https://github.com/grknylmz/sap-ai-dev-toolkit).
|
|
1110
1132
|
|
|
1111
1133
|
## π Publishing to npm
|
package/dist/checksums.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
8666b8126f460a6b1b3c64c5523eb5b942d2c1a795f406936cfb9e0f1c62e68c vsp-linux-x64
|
|
2
|
+
e920aa3fda3b3b5020893e0869737c8a2ca049db9eafdd9e19dfca021cefe24b vsp-linux-arm64
|
|
3
|
+
e56bf5a23cdbf889bff499a399b53fd247b2ef26b44e0d4affa6a20382303abd vsp-darwin-x64
|
|
4
|
+
3c794b489af29237480a27353cb3e98af35de1d81ad03761304da10fdb5df89d vsp-darwin-arm64
|
|
5
|
+
0c35ddd18c2edfb10486a056143ad438d79a737f55839e3adfb882b29438fa1a vsp-win32-x64.exe
|
package/dist/vsp-darwin-arm64
CHANGED
|
Binary file
|
package/dist/vsp-darwin-x64
CHANGED
|
Binary file
|
package/dist/vsp-linux-arm64
CHANGED
|
Binary file
|
package/dist/vsp-linux-x64
CHANGED
|
Binary file
|
package/dist/vsp-win32-x64.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
diff --git a/pkg/adt/http.go b/pkg/adt/http.go
|
|
2
|
+
index e420573..5386a26 100755
|
|
3
|
+
--- a/pkg/adt/http.go
|
|
4
|
+
+++ b/pkg/adt/http.go
|
|
5
|
+
@@ -12,6 +12,7 @@ import (
|
|
6
|
+
"os"
|
|
7
|
+
"strings"
|
|
8
|
+
"sync"
|
|
9
|
+
+ "sync/atomic"
|
|
10
|
+
"time"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
@@ -135,6 +136,10 @@ type Transport struct {
|
|
14
|
+
// from triggering simultaneous SAML dances.
|
|
15
|
+
reauthMu sync.Mutex
|
|
16
|
+
lastReauth time.Time
|
|
17
|
+
+
|
|
18
|
+
+ // Logon single-flight: see http_logon_gate.go.
|
|
19
|
+
+ logonSlot chan struct{}
|
|
20
|
+
+ logonSettled atomic.Bool
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// NewTransport creates a new Transport with the given configuration.
|
|
24
|
+
@@ -149,6 +154,7 @@ func NewTransportWithClient(cfg *Config, client HTTPDoer) *Transport {
|
|
25
|
+
t := &Transport{
|
|
26
|
+
config: cfg,
|
|
27
|
+
httpClient: client,
|
|
28
|
+
+ logonSlot: make(chan struct{}, 1),
|
|
29
|
+
}
|
|
30
|
+
if cfg.Cache {
|
|
31
|
+
t.cache = newResponseCache(cfg.CacheStore, cfg.CacheTTL)
|
|
32
|
+
@@ -212,10 +218,10 @@ func (t *Transport) Request(ctx context.Context, path string, opts *RequestOptio
|
|
33
|
+
opts.Method = http.MethodGet
|
|
34
|
+
}
|
|
35
|
+
if t.cache == nil {
|
|
36
|
+
- return t.request(ctx, path, opts)
|
|
37
|
+
+ return t.gatedRequest(ctx, path, opts)
|
|
38
|
+
}
|
|
39
|
+
if !cacheable(path, opts) {
|
|
40
|
+
- resp, err := t.request(ctx, path, opts)
|
|
41
|
+
+ resp, err := t.gatedRequest(ctx, path, opts)
|
|
42
|
+
if isModifyingMethod(opts.Method) && !strings.HasPrefix(path, "/sap/bc/adt/datapreview/") {
|
|
43
|
+
t.cache.invalidate()
|
|
44
|
+
}
|
|
45
|
+
@@ -229,7 +235,7 @@ func (t *Transport) Request(ctx context.Context, path string, opts *RequestOptio
|
|
46
|
+
if resp, ok := t.cache.get(key); ok {
|
|
47
|
+
return resp, nil
|
|
48
|
+
}
|
|
49
|
+
- resp, err := t.request(ctx, path, opts)
|
|
50
|
+
+ resp, err := t.gatedRequest(ctx, path, opts)
|
|
51
|
+
if err == nil && resp != nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
52
|
+
t.cache.put(key, resp)
|
|
53
|
+
}
|
|
54
|
+
@@ -1062,6 +1068,8 @@ func (t *Transport) resetCookieJar() {
|
|
55
|
+
}
|
|
56
|
+
if jar, err := cookiejar.New(nil); err == nil {
|
|
57
|
+
client.Jar = jar
|
|
58
|
+
+ // The new jar holds no session: the next requests must share one logon again.
|
|
59
|
+
+ t.logonSettled.Store(false)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
diff --git a/pkg/adt/http_logon_gate.go b/pkg/adt/http_logon_gate.go
|
|
64
|
+
new file mode 100644
|
|
65
|
+
index 0000000..9e71c60
|
|
66
|
+
--- /dev/null
|
|
67
|
+
+++ b/pkg/adt/http_logon_gate.go
|
|
68
|
+
@@ -0,0 +1,76 @@
|
|
69
|
+
+package adt
|
|
70
|
+
+
|
|
71
|
+
+import (
|
|
72
|
+
+ "context"
|
|
73
|
+
+ "net/http"
|
|
74
|
+
+ "net/url"
|
|
75
|
+
+ "strings"
|
|
76
|
+
+)
|
|
77
|
+
+
|
|
78
|
+
+// Every request that reaches SAP without a security-session cookie is a new
|
|
79
|
+
+// logon: SAP authenticates it and opens a security session of its own, which
|
|
80
|
+
+// lingers in SM04 until it times out. MCP clients fan tool calls out in
|
|
81
|
+
+// parallel, so a cold transport used to send each of them without a cookie
|
|
82
|
+
+// and opened one session per call (dozens per burst for one user).
|
|
83
|
+
+//
|
|
84
|
+
+// gatedRequest lets exactly one request log on while the jar holds no session
|
|
85
|
+
+// cookie; the others wait for it and then reuse the cookie it brought back.
|
|
86
|
+
+// When that request succeeds without the server issuing a session cookie
|
|
87
|
+
+// (security sessions off, or a proxy chain that keeps them), the gate settles
|
|
88
|
+
+// open so requests are never serialized for nothing. Dropping the jar after an
|
|
89
|
+
+// expired session re-arms it.
|
|
90
|
+
+func (t *Transport) gatedRequest(ctx context.Context, path string, opts *RequestOptions) (*Response, error) {
|
|
91
|
+
+ if t.logonSlot == nil || t.logonSettled.Load() || t.hasSessionCookie() {
|
|
92
|
+
+ return t.request(ctx, path, opts)
|
|
93
|
+
+ }
|
|
94
|
+
+ select {
|
|
95
|
+
+ case t.logonSlot <- struct{}{}:
|
|
96
|
+
+ case <-ctx.Done():
|
|
97
|
+
+ return nil, ctx.Err()
|
|
98
|
+
+ }
|
|
99
|
+
+ if t.logonSettled.Load() || t.hasSessionCookie() {
|
|
100
|
+
+ <-t.logonSlot
|
|
101
|
+
+ return t.request(ctx, path, opts)
|
|
102
|
+
+ }
|
|
103
|
+
+ resp, err := t.request(ctx, path, opts)
|
|
104
|
+
+ if err == nil && !t.hasSessionCookie() {
|
|
105
|
+
+ t.logonSettled.Store(true)
|
|
106
|
+
+ }
|
|
107
|
+
+ <-t.logonSlot
|
|
108
|
+
+ return resp, err
|
|
109
|
+
+}
|
|
110
|
+
+
|
|
111
|
+
+// hasSessionCookie reports whether requests to the SAP system already carry
|
|
112
|
+
+// a security session: one the jar learned from the server, or one the caller
|
|
113
|
+
+// supplied for cookie authentication.
|
|
114
|
+
+func (t *Transport) hasSessionCookie() bool {
|
|
115
|
+
+ t.cookiesMu.RLock()
|
|
116
|
+
+ for name := range t.config.Cookies {
|
|
117
|
+
+ if isSessionCookieName(name) {
|
|
118
|
+
+ t.cookiesMu.RUnlock()
|
|
119
|
+
+ return true
|
|
120
|
+
+ }
|
|
121
|
+
+ }
|
|
122
|
+
+ t.cookiesMu.RUnlock()
|
|
123
|
+
+ client, ok := t.httpClient.(*http.Client)
|
|
124
|
+
+ if !ok || client.Jar == nil {
|
|
125
|
+
+ return false
|
|
126
|
+
+ }
|
|
127
|
+
+ base, err := url.Parse(strings.TrimRight(t.config.BaseURL, "/") + "/sap/bc/adt/")
|
|
128
|
+
+ if err != nil {
|
|
129
|
+
+ return false
|
|
130
|
+
+ }
|
|
131
|
+
+ for _, cookie := range client.Jar.Cookies(base) {
|
|
132
|
+
+ if isSessionCookieName(cookie.Name) {
|
|
133
|
+
+ return true
|
|
134
|
+
+ }
|
|
135
|
+
+ }
|
|
136
|
+
+ return false
|
|
137
|
+
+}
|
|
138
|
+
+
|
|
139
|
+
+// isSessionCookieName matches the ICF security-session cookie
|
|
140
|
+
+// (SAP_SESSIONID_<SID>_<client>). A MYSAPSSO2 logon ticket does not count:
|
|
141
|
+
+// every request presenting only the ticket opens another session.
|
|
142
|
+
+func isSessionCookieName(name string) bool {
|
|
143
|
+
+ return strings.HasPrefix(name, "SAP_SESSIONID")
|
|
144
|
+
+}
|
|
145
|
+
diff --git a/pkg/adt/http_logon_gate_test.go b/pkg/adt/http_logon_gate_test.go
|
|
146
|
+
new file mode 100644
|
|
147
|
+
index 0000000..e973790
|
|
148
|
+
--- /dev/null
|
|
149
|
+
+++ b/pkg/adt/http_logon_gate_test.go
|
|
150
|
+
@@ -0,0 +1,108 @@
|
|
151
|
+
+package adt
|
|
152
|
+
+
|
|
153
|
+
+import (
|
|
154
|
+
+ "context"
|
|
155
|
+
+ "fmt"
|
|
156
|
+
+ "net/http"
|
|
157
|
+
+ "net/http/httptest"
|
|
158
|
+
+ "sync"
|
|
159
|
+
+ "sync/atomic"
|
|
160
|
+
+ "testing"
|
|
161
|
+
+ "time"
|
|
162
|
+
+)
|
|
163
|
+
+
|
|
164
|
+
+// sapSessionServer mimics ICF security sessions: a request without a live
|
|
165
|
+
+// SAP_SESSIONID cookie is a logon and, when issueCookies, gets a new one.
|
|
166
|
+
+type sapSessionServer struct {
|
|
167
|
+
+ issueCookies bool
|
|
168
|
+
+ logons atomic.Int32
|
|
169
|
+
+ inFlight atomic.Int32
|
|
170
|
+
+ peak atomic.Int32
|
|
171
|
+
+ mu sync.Mutex
|
|
172
|
+
+ live map[string]bool
|
|
173
|
+
+}
|
|
174
|
+
+
|
|
175
|
+
+func (s *sapSessionServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
176
|
+
+ now := s.inFlight.Add(1)
|
|
177
|
+
+ defer s.inFlight.Add(-1)
|
|
178
|
+
+ for {
|
|
179
|
+
+ peak := s.peak.Load()
|
|
180
|
+
+ if now <= peak || s.peak.CompareAndSwap(peak, now) {
|
|
181
|
+
+ break
|
|
182
|
+
+ }
|
|
183
|
+
+ }
|
|
184
|
+
+ s.mu.Lock()
|
|
185
|
+
+ cookie, err := r.Cookie("SAP_SESSIONID_T4D_100")
|
|
186
|
+
+ if err != nil || !s.live[cookie.Value] {
|
|
187
|
+
+ id := fmt.Sprintf("S%d", s.logons.Add(1))
|
|
188
|
+
+ if s.issueCookies {
|
|
189
|
+
+ s.live[id] = true
|
|
190
|
+
+ http.SetCookie(w, &http.Cookie{Name: "SAP_SESSIONID_T4D_100", Value: id, Path: "/"})
|
|
191
|
+
+ }
|
|
192
|
+
+ }
|
|
193
|
+
+ s.mu.Unlock()
|
|
194
|
+
+ w.Header().Add("Set-Cookie", "sap-usercontext=sap-client=100; path=/")
|
|
195
|
+
+ time.Sleep(30 * time.Millisecond)
|
|
196
|
+
+ fmt.Fprint(w, "ok")
|
|
197
|
+
+}
|
|
198
|
+
+
|
|
199
|
+
+func newSessionTransport(t *testing.T, issueCookies bool) (*Transport, *sapSessionServer) {
|
|
200
|
+
+ t.Helper()
|
|
201
|
+
+ sap := &sapSessionServer{issueCookies: issueCookies, live: map[string]bool{}}
|
|
202
|
+
+ server := httptest.NewServer(sap)
|
|
203
|
+
+ t.Cleanup(server.Close)
|
|
204
|
+
+ return NewTransport(NewConfig(server.URL, "EMSDI0845", "secret", WithClient("100"))), sap
|
|
205
|
+
+}
|
|
206
|
+
+
|
|
207
|
+
+func burst(t *testing.T, transport *Transport, n int) {
|
|
208
|
+
+ t.Helper()
|
|
209
|
+
+ var wg sync.WaitGroup
|
|
210
|
+
+ errs := make(chan error, n)
|
|
211
|
+
+ for i := range n {
|
|
212
|
+
+ wg.Add(1)
|
|
213
|
+
+ go func() {
|
|
214
|
+
+ defer wg.Done()
|
|
215
|
+
+ if _, err := transport.Request(context.Background(), fmt.Sprintf("/sap/bc/adt/oo/classes/ZCL_%d/source/main", i), nil); err != nil {
|
|
216
|
+
+ errs <- err
|
|
217
|
+
+ }
|
|
218
|
+
+ }()
|
|
219
|
+
+ }
|
|
220
|
+
+ wg.Wait()
|
|
221
|
+
+ close(errs)
|
|
222
|
+
+ for err := range errs {
|
|
223
|
+
+ t.Fatalf("request failed: %v", err)
|
|
224
|
+
+ }
|
|
225
|
+
+}
|
|
226
|
+
+
|
|
227
|
+
+func TestTransport_ConcurrentColdRequestsShareOneLogon(t *testing.T) {
|
|
228
|
+
+ transport, sap := newSessionTransport(t, true)
|
|
229
|
+
+
|
|
230
|
+
+ burst(t, transport, 12)
|
|
231
|
+
+ burst(t, transport, 12)
|
|
232
|
+
+
|
|
233
|
+
+ if got := sap.logons.Load(); got != 1 {
|
|
234
|
+
+ t.Fatalf("logons = %d, want 1: parallel tool calls must reuse one SAP security session", got)
|
|
235
|
+
+ }
|
|
236
|
+
+}
|
|
237
|
+
+
|
|
238
|
+
+func TestTransport_LogonGateOpensWhenServerKeepsNoSession(t *testing.T) {
|
|
239
|
+
+ transport, sap := newSessionTransport(t, false)
|
|
240
|
+
+
|
|
241
|
+
+ burst(t, transport, 12)
|
|
242
|
+
+
|
|
243
|
+
+ if peak := sap.peak.Load(); peak < 2 {
|
|
244
|
+
+ t.Fatalf("peak concurrency = %d: without session cookies the gate must not serialize every request", peak)
|
|
245
|
+
+ }
|
|
246
|
+
+}
|
|
247
|
+
+
|
|
248
|
+
+func TestTransport_LogonGateRearmsAfterJarReset(t *testing.T) {
|
|
249
|
+
+ transport, sap := newSessionTransport(t, true)
|
|
250
|
+
+
|
|
251
|
+
+ burst(t, transport, 6)
|
|
252
|
+
+ transport.resetCookieJar()
|
|
253
|
+
+ burst(t, transport, 6)
|
|
254
|
+
+
|
|
255
|
+
+ if got := sap.logons.Load(); got != 2 {
|
|
256
|
+
+ t.Fatalf("logons = %d, want 2 (one per session)", got)
|
|
257
|
+
+ }
|
|
258
|
+
+}
|
package/scripts/build-vsp.mjs
CHANGED
|
@@ -33,7 +33,7 @@ async function main() {
|
|
|
33
33
|
await mkdir(dirname(source), { recursive: true });
|
|
34
34
|
await run('git', ['clone', '--filter=blob:none', upstream, source]);
|
|
35
35
|
await run('git', ['-C', source, 'checkout', '--detach', commit]);
|
|
36
|
-
for (const patchFile of ['vsp-bas-proxy-auth.patch', 'vsp-namespaced-class-include-edit.patch']) {
|
|
36
|
+
for (const patchFile of ['vsp-bas-proxy-auth.patch', 'vsp-namespaced-class-include-edit.patch', 'vsp-single-logon.patch']) {
|
|
37
37
|
await run('patch', ['-p1', '-i', join(root, 'patches', patchFile)], { cwd: source });
|
|
38
38
|
}
|
|
39
39
|
await mkdir(dist, { recursive: true });
|