sap-ai-dev-toolkit 0.5.0 → 0.5.6

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.
Files changed (57) hide show
  1. package/.github/agents/abap-developer.agent.md +3 -3
  2. package/.github/agents/abap-runtime-debugger.agent.md +3 -3
  3. package/.github/agents/hana-cloud-hdi-specialist.agent.md +1 -1
  4. package/.github/agents/rap-service-developer.agent.md +1 -1
  5. package/.github/agents/sap-solution-architect.agent.md +1 -1
  6. package/.github/skills/abap-debugging/SKILL.md +3 -3
  7. package/.github/skills/abap-development/SKILL.md +1 -1
  8. package/.github/skills/abap-runtime-analysis/SKILL.md +3 -3
  9. package/.github/skills/abap-testing-quality/SKILL.md +1 -1
  10. package/.github/skills/cds-development/SKILL.md +1 -1
  11. package/.github/skills/clean-core-extensibility/SKILL.md +1 -1
  12. package/.github/skills/hana-cloud-inspection/SKILL.md +1 -1
  13. package/.github/skills/hana-cloud-native-development/SKILL.md +2 -2
  14. package/.github/skills/hana-cloud-validation/SKILL.md +1 -1
  15. package/.github/skills/rap-development/SKILL.md +1 -1
  16. package/.github/skills/rap-service-delivery/SKILL.md +1 -1
  17. package/.github/skills/sap-sdlc-orchestration/SKILL.md +1 -1
  18. package/.github/skills/sap-standard-api-analysis/SKILL.md +1 -1
  19. package/.github/skills/sap-transport-release/SKILL.md +1 -1
  20. package/README.md +17 -11
  21. package/inventory.md +179 -0
  22. package/package.json +2 -2
  23. package/scripts/build-vsp.mjs +2 -1
  24. package/scripts/postinstall.mjs +17 -18
  25. package/src/bas-destination-relay.mjs +58 -22
  26. package/src/bas-discovery.mjs +24 -8
  27. package/src/binary.mjs +101 -17
  28. package/src/cf-destination.mjs +55 -1
  29. package/src/credentials-store.mjs +0 -10
  30. package/src/engineering-tools.mjs +32 -10
  31. package/src/hana-tools.mjs +9 -2
  32. package/src/launcher.mjs +22 -19
  33. package/src/mcp-config.mjs +41 -7
  34. package/src/mcp-proxy.mjs +392 -104
  35. package/src/redact.mjs +28 -0
  36. package/src/setup.mjs +33 -3
  37. package/tools.md +4 -2
  38. package/test/bas-destination-relay.test.mjs +0 -374
  39. package/test/cf-connectivity.test.mjs +0 -287
  40. package/test/cf-destination.test.mjs +0 -307
  41. package/test/cf-runtime.test.mjs +0 -527
  42. package/test/copilot-assets.test.mjs +0 -63
  43. package/test/copilot-content.test.mjs +0 -249
  44. package/test/credentials-store.test.mjs +0 -187
  45. package/test/discovery.test.mjs +0 -257
  46. package/test/fixtures/fake-vsp.mjs +0 -263
  47. package/test/hana-config.test.mjs +0 -168
  48. package/test/hana-inspector-stdio.test.mjs +0 -44
  49. package/test/hana-tools.test.mjs +0 -206
  50. package/test/launcher.test.mjs +0 -399
  51. package/test/live-s4h.test.mjs +0 -202
  52. package/test/mcp-config-cf.test.mjs +0 -407
  53. package/test/mcp-proxy.test.mjs +0 -409
  54. package/test/pty.mjs +0 -8
  55. package/test/setup-cf.test.mjs +0 -489
  56. package/test/setup.test.mjs +0 -417
  57. package/test/terminal-ui.test.mjs +0 -47
@@ -1,527 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
- import { createServer, request as httpRequest } from 'node:http';
5
- import { spawn } from 'node:child_process';
6
- import { tmpdir } from 'node:os';
7
- import { join } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { storeDestinationCredentials } from '../src/credentials-store.mjs';
10
-
11
- const launcher = fileURLToPath(new URL('../src/launcher.mjs', import.meta.url));
12
- const fakeVsp = fileURLToPath(new URL('./fixtures/fake-vsp.mjs', import.meta.url));
13
- const SPACE_GUID = 'runtime-space-guid';
14
- const INSTANCE_GUID = 'runtime-destination-guid';
15
- const INSTANCE_NAME = 'runtime-destination-service';
16
-
17
- async function listen(server) {
18
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
19
- return `http://127.0.0.1:${server.address().port}`;
20
- }
21
-
22
- function runLauncher(env, args = [], messages = []) {
23
- return new Promise((resolve, reject) => {
24
- const child = spawn(process.execPath, [launcher, ...args], { env, stdio: ['pipe', 'pipe', 'pipe'] });
25
- let stdout = '';
26
- let stderr = '';
27
- child.stdout.setEncoding('utf8');
28
- child.stderr.setEncoding('utf8');
29
- child.stdout.on('data', chunk => { stdout += chunk; });
30
- child.stderr.on('data', chunk => { stderr += chunk; });
31
- child.once('error', reject);
32
- child.once('exit', (code, signal) => resolve({ code, signal, stdout, stderr }));
33
- if (messages.length) child.stdin.end(`${messages.map(message => JSON.stringify(message)).join('\n')}\n`);
34
- else child.stdin.end();
35
- });
36
- }
37
-
38
- async function fakeCfExecutable(directory, origin, callLog, { connectivityProxyPort = 0, connectivityTokenUrl = `${origin}/connectivity/oauth/token` } = {}) {
39
- const path = join(directory, 'cf');
40
- const source = `#!/usr/bin/env node
41
- const fs = require('node:fs');
42
- const args = process.argv.slice(2);
43
- if (process.env.CF_CALL_LOG) fs.appendFileSync(process.env.CF_CALL_LOG, JSON.stringify(args) + '\\n');
44
- const origin = process.env.CF_TEST_ORIGIN;
45
- const spaceGuid = process.env.CF_TEST_SPACE_GUID;
46
- const instanceGuid = process.env.CF_TEST_INSTANCE_GUID;
47
- const instanceName = process.env.CF_TEST_INSTANCE_NAME;
48
- const connectivityGuid = process.env.CF_TEST_CONNECTIVITY_GUID;
49
- const connectivityName = process.env.CF_TEST_CONNECTIVITY_NAME;
50
- if (args[0] === 'version') process.stdout.write('cf version 8.18.0+test\\n');
51
- else if (args[0] === 'target') process.stdout.write('space: MASS\\n');
52
- else if (args[0] === 'space' && args[1] === 'MASS' && args[2] === '--guid') process.stdout.write(spaceGuid + '\\n');
53
- else if (args[0] === 'oauth-token') process.stdout.write('bearer current-cf-user-jwt\\n');
54
- else if (args[0] === 'curl' && args[1].startsWith('/v3/service_instances/')) {
55
- const guid = args[1].split('/').at(-1);
56
- const name = guid === connectivityGuid ? connectivityName : instanceName;
57
- process.stdout.write(JSON.stringify({ guid, name, relationships: { space: { data: { guid: spaceGuid } } } }));
58
- }
59
- else if (args[0] === 'service-key' && args[1] === '--json' && args[2] === connectivityName) {
60
- process.stdout.write(JSON.stringify({ credentials: {
61
- onpremise_proxy_host: '127.0.0.1',
62
- onpremise_proxy_http_port: Number(process.env.CF_TEST_CONNECTIVITY_PROXY_PORT),
63
- token_service_url: process.env.CF_TEST_CONNECTIVITY_TOKEN_URL,
64
- clientid: 'connectivity-client',
65
- clientsecret: 'connectivity-key-secret'
66
- } }));
67
- }
68
- else if (args[0] === 'service-key' && args[1] === '--json') process.stdout.write(JSON.stringify({ credentials: { uri: origin, url: origin + '/uaa', clientid: 'destination-client', clientsecret: 'destination-key-secret' } }));
69
- else process.exitCode = 2;
70
- `;
71
- await writeFile(path, source, { mode: 0o755 });
72
- await chmod(path, 0o755);
73
- return {
74
- PATH: `${directory}:${process.env.PATH}`,
75
- CF_TEST_ORIGIN: origin,
76
- CF_TEST_SPACE_GUID: SPACE_GUID,
77
- CF_TEST_INSTANCE_GUID: INSTANCE_GUID,
78
- CF_TEST_INSTANCE_NAME: INSTANCE_NAME,
79
- CF_TEST_CONNECTIVITY_GUID: 'runtime-connectivity-guid',
80
- CF_TEST_CONNECTIVITY_NAME: 'runtime-connectivity-service',
81
- CF_TEST_CONNECTIVITY_PROXY_PORT: String(connectivityProxyPort),
82
- CF_TEST_CONNECTIVITY_TOKEN_URL: connectivityTokenUrl,
83
- CF_CALL_LOG: callLog
84
- };
85
- }
86
-
87
- function destinationServiceServer({ destinationName = 'cf-basic', client = '321' } = {}) {
88
- const observed = { token: [], list: [] };
89
- const server = createServer((request, response) => {
90
- if (request.url === '/uaa/oauth/token') {
91
- observed.token.push(request.headers.authorization);
92
- response.writeHead(200, { 'content-type': 'application/json' });
93
- response.end(JSON.stringify({ access_token: 'destination-api-token', expires_in: 300 }));
94
- return;
95
- }
96
- if (request.url === '/destination-configuration/v1/instanceDestinations') {
97
- observed.list.push(request.headers.authorization);
98
- response.writeHead(200, { 'content-type': 'application/json' });
99
- response.end(JSON.stringify([{
100
- Name: destinationName,
101
- Type: 'HTTP',
102
- URL: 'https://sap.example:443',
103
- Authentication: 'BasicAuthentication',
104
- ProxyType: 'Internet',
105
- Properties: [
106
- { Key: 'sap-client', Value: client },
107
- { Key: 'User', Value: 'destination-user' },
108
- { Key: 'Password', Value: 'destination-sap-password' }
109
- ]
110
- }]));
111
- return;
112
- }
113
- response.writeHead(404);
114
- response.end();
115
- });
116
- return { server, observed };
117
- }
118
-
119
- test('launches a CF Basic destination with per-child credentials and no proxy-auth argument', async t => {
120
- const directory = await mkdtemp(join(tmpdir(), 'bas-cf-runtime-'));
121
- const api = destinationServiceServer();
122
- const origin = await listen(api.server);
123
- const callLog = join(directory, 'cf-calls.jsonl');
124
- const cfEnv = await fakeCfExecutable(directory, origin, callLog);
125
- const childLog = join(directory, 'vsp-children.jsonl');
126
- t.after(async () => {
127
- await new Promise(resolve => api.server.close(resolve));
128
- await rm(directory, { recursive: true, force: true });
129
- });
130
- const env = {
131
- ...process.env,
132
- ...cfEnv,
133
- SAP_AI_DEV_TOOLKIT_BINARY: fakeVsp,
134
- SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
135
- SAP_AI_DEV_TOOLKIT_DESTINATION: 'cf-basic',
136
- BAS_CF_SPACE_GUID: SPACE_GUID,
137
- BAS_CF_DESTINATION_INSTANCE_GUID: INSTANCE_GUID,
138
- BAS_CF_DESTINATION_INSTANCE: INSTANCE_NAME,
139
- BAS_CF_DESTINATION_KEY: 'managed-destination-key',
140
- BAS_CF_DESTINATION_NAME: 'cf-basic',
141
- H2O_URL: 'http://retained-for-checks.example',
142
- SAP_USER: 'parent-user-secret',
143
- SAP_PASSWORD: 'parent-password-secret',
144
- Authorization: 'Bearer parent-token-secret',
145
- Cookie: 'parent-cookie-secret',
146
- FAKE_LOG: childLog,
147
- HTTP_PROXY: '',
148
- HTTPS_PROXY: '',
149
- NO_PROXY: '127.0.0.1,localhost'
150
- };
151
- const result = await runLauncher(env, [], [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05' } }]);
152
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
153
- const events = (await readFile(childLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
154
- const initialized = events.find(event => event.event === 'initialize');
155
- assert.ok(initialized);
156
- assert.equal(initialized.argv[initialized.argv.indexOf('--url') + 1], 'https://sap.example/');
157
- assert.equal(initialized.argv.includes('--proxy-auth'), false);
158
- assert.equal(JSON.stringify(initialized.argv).includes('destination-sap-password'), false);
159
- assert.equal(JSON.stringify(initialized.argv).includes('destination-user'), false);
160
- assert.deepEqual([initialized.env.user, initialized.env.password, initialized.env.verbose], [
161
- 'destination-user', 'destination-sap-password', 'false'
162
- ]);
163
- assert.equal(initialized.env.authorization, undefined);
164
- assert.equal(initialized.env.cookie, undefined);
165
- for (const secret of [
166
- 'destination-key-secret', 'destination-api-token', 'destination-sap-password', 'destination-user',
167
- 'parent-user-secret', 'parent-password-secret', 'parent-token-secret', 'parent-cookie-secret'
168
- ]) {
169
- assert.equal(`${result.stdout}\\n${result.stderr}\\n${JSON.stringify(initialized.argv)}`.includes(secret), false, `${secret} must not appear in output or argv`);
170
- }
171
- assert.deepEqual(api.observed.token, [`Basic ${Buffer.from('destination-client:destination-key-secret').toString('base64')}`]);
172
- assert.deepEqual(api.observed.list, ['Bearer destination-api-token']);
173
- const cliCalls = (await readFile(callLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
174
- assert.ok(cliCalls.some(args => args[0] === 'service-key' && args.at(-1) === 'managed-destination-key'));
175
- assert.equal(JSON.stringify(cliCalls).includes('destination-key-secret'), false);
176
- });
177
- test('routes a PrincipalPropagation MCP call through CF Connectivity to the SAP target', async t => {
178
- const directory = await mkdtemp(join(tmpdir(), 'bas-cf-principal-'));
179
- const observed = { destinationToken: [], destinationList: [], connectivityToken: [], proxy: [], target: [] };
180
- const target = createServer((request, response) => {
181
- observed.target.push({ path: request.url, authorization: request.headers.authorization });
182
- response.writeHead(200, { 'content-type': 'text/plain' });
183
- response.end('SAP system information: user=cf-user-42');
184
- });
185
- const targetOrigin = await listen(target);
186
- const service = createServer(async (request, response) => {
187
- if (request.url === '/uaa/oauth/token') {
188
- observed.destinationToken.push(request.headers.authorization);
189
- response.writeHead(200, { 'content-type': 'application/json' });
190
- response.end(JSON.stringify({ access_token: 'destination-api-token', expires_in: 300 }));
191
- } else if (request.url === '/destination-configuration/v1/instanceDestinations') {
192
- observed.destinationList.push(request.headers.authorization);
193
- response.writeHead(200, { 'content-type': 'application/json' });
194
- response.end(JSON.stringify([{
195
- Name: 'pp-target',
196
- Type: 'HTTP',
197
- URL: targetOrigin,
198
- Authentication: 'PrincipalPropagation',
199
- ProxyType: 'OnPremise',
200
- Properties: [
201
- { Key: 'sap-client', Value: '100' },
202
- { Key: 'CloudConnectorLocationId', Value: 'scc-north' }
203
- ]
204
- }]));
205
- } else if (request.url === '/connectivity/oauth/token') {
206
- const chunks = [];
207
- for await (const chunk of request) chunks.push(chunk);
208
- observed.connectivityToken.push({
209
- authorization: request.headers.authorization,
210
- body: new URLSearchParams(Buffer.concat(chunks).toString())
211
- });
212
- response.writeHead(200, { 'content-type': 'application/json' });
213
- response.end(JSON.stringify({ access_token: 'user-exchange-token', expires_in: 300 }));
214
- } else {
215
- response.writeHead(404);
216
- response.end();
217
- }
218
- });
219
- const serviceOrigin = await listen(service);
220
- const connectivityProxy = createServer((request, response) => {
221
- observed.proxy.push({
222
- authorization: request.headers['proxy-authorization'],
223
- location: request.headers['sap-connectivity-scc-location_id'],
224
- path: request.url
225
- });
226
- if (request.headers['proxy-authorization'] !== 'Bearer user-exchange-token' || request.headers['sap-connectivity-scc-location_id'] !== 'scc-north') {
227
- response.writeHead(403);
228
- response.end('Connectivity authorization rejected');
229
- request.resume();
230
- return;
231
- }
232
- const targetUrl = new URL(request.url);
233
- const headers = { ...request.headers, host: targetUrl.host };
234
- delete headers['proxy-authorization'];
235
- delete headers['sap-connectivity-scc-location_id'];
236
- delete headers.connection;
237
- const upstream = httpRequest({
238
- hostname: targetUrl.hostname,
239
- port: Number(targetUrl.port || 80),
240
- method: request.method,
241
- path: `${targetUrl.pathname}${targetUrl.search}`,
242
- headers
243
- }, upstreamResponse => {
244
- response.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
245
- upstreamResponse.pipe(response);
246
- });
247
- upstream.on('error', () => {
248
- if (!response.headersSent) response.writeHead(502);
249
- response.end();
250
- });
251
- upstream.end();
252
- request.resume();
253
- });
254
- const connectivityProxyOrigin = await listen(connectivityProxy);
255
- const connectivityProxyPort = Number(new URL(connectivityProxyOrigin).port);
256
- const cfCalls = join(directory, 'cf-calls.jsonl');
257
- const cfEnv = await fakeCfExecutable(directory, serviceOrigin, cfCalls, {
258
- connectivityProxyPort,
259
- connectivityTokenUrl: `${serviceOrigin}/connectivity/oauth/token`
260
- });
261
- const credentialsPath = join(directory, 'sap-ai-dev-toolkit-credentials.json');
262
- await storeDestinationCredentials(credentialsPath, `cloud-foundry:cf:${SPACE_GUID}:${INSTANCE_GUID}:pp-target`, {
263
- user: 'must-not-override',
264
- password: 'must-not-override-secret',
265
- mode: 'cf-connectivity'
266
- });
267
- const childLog = join(directory, 'vsp-children.jsonl');
268
- t.after(async () => {
269
- await new Promise(resolve => service.close(resolve));
270
- await new Promise(resolve => target.close(resolve));
271
- await new Promise(resolve => connectivityProxy.close(resolve));
272
- await rm(directory, { recursive: true, force: true });
273
- });
274
-
275
- const result = await runLauncher({
276
- ...process.env,
277
- ...cfEnv,
278
- SAP_AI_DEV_TOOLKIT_BINARY: fakeVsp,
279
- SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
280
- SAP_AI_DEV_TOOLKIT_DESTINATION: 'pp-target',
281
- SAP_AI_DEV_TOOLKIT_CREDENTIALS_FILE: credentialsPath,
282
- BAS_CF_SPACE_GUID: SPACE_GUID,
283
- BAS_CF_DESTINATION_INSTANCE_GUID: INSTANCE_GUID,
284
- BAS_CF_DESTINATION_INSTANCE: INSTANCE_NAME,
285
- BAS_CF_DESTINATION_KEY: 'managed-destination-key',
286
- BAS_CF_DESTINATION_NAME: 'pp-target',
287
- BAS_CF_CONNECTIVITY_INSTANCE_GUID: 'runtime-connectivity-guid',
288
- BAS_CF_CONNECTIVITY_INSTANCE: 'runtime-connectivity-service',
289
- BAS_CF_CONNECTIVITY_KEY: 'managed-connectivity-key',
290
- H2O_URL: 'http://retained-for-checks.example',
291
- SAP_USER: 'parent-user-secret',
292
- SAP_PASSWORD: 'parent-password-secret',
293
- FAKE_LOG: childLog,
294
- FAKE_CF_ADT_TEST: 'true',
295
- HTTP_PROXY: '',
296
- HTTPS_PROXY: '',
297
- NO_PROXY: '127.0.0.1,localhost'
298
- }, [], [
299
- { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05' } },
300
- { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} },
301
- { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'pp-target__GetSystemInfo', arguments: {} } }
302
- ]);
303
- assert.equal(result.code, 0, `${result.stdout}\\n${result.stderr}`);
304
- const responses = result.stdout.trim().split('\n').map(line => JSON.parse(line));
305
- assert.ok(responses.at(-1).result, `${result.stdout}\n${result.stderr}`);
306
- assert.match(responses.at(-1).result.content[0].text, /SAP system information: user=cf-user-42/);
307
- assert.deepEqual(observed.destinationToken, [`Basic ${Buffer.from('destination-client:destination-key-secret').toString('base64')}`]);
308
- assert.deepEqual(observed.destinationList, ['Bearer destination-api-token']);
309
- assert.equal(observed.connectivityToken.length, 1);
310
- assert.equal(observed.connectivityToken[0].authorization, `Basic ${Buffer.from('connectivity-client:connectivity-key-secret').toString('base64')}`);
311
- assert.deepEqual(Object.fromEntries(observed.connectivityToken[0].body), {
312
- grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
313
- assertion: 'current-cf-user-jwt',
314
- token_format: 'jwt',
315
- response_type: 'token'
316
- });
317
- assert.deepEqual(observed.proxy.map(request => [request.authorization, request.location]), [['Bearer user-exchange-token', 'scc-north']]);
318
- assert.deepEqual(observed.target, [{ path: '/sap/bc/adt/discovery', authorization: undefined }]);
319
- const events = (await readFile(childLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
320
- const initialized = events.find(event => event.event === 'initialize');
321
- assert.equal(initialized.argv.includes('--proxy-auth'), true);
322
- assert.equal(initialized.env.user, undefined);
323
- assert.equal(initialized.env.password, undefined);
324
- for (const secret of ['must-not-override', 'must-not-override-secret', 'destination-key-secret', 'destination-api-token', 'connectivity-key-secret', 'user-exchange-token', 'current-cf-user-jwt', 'parent-user-secret', 'parent-password-secret']) {
325
- assert.equal(`${result.stdout}\\n${result.stderr}\\n${JSON.stringify(initialized.argv)}`.includes(secret), false);
326
- }
327
- });
328
-
329
- test('overrides CF Basic OnPremise backend auth while retaining the HTTP Connectivity proxy', async t => {
330
- const directory = await mkdtemp(join(tmpdir(), 'bas-cf-basic-onprem-'));
331
- const observed = { destinationToken: [], destinationList: [], connectivityToken: [], proxy: [], connect: [], target: [] };
332
- const target = createServer((request, response) => {
333
- observed.target.push({ path: request.url, authorization: request.headers.authorization });
334
- response.writeHead(request.headers.authorization === `Basic ${Buffer.from('override-user:override-password').toString('base64')}` ? 200 : 401, {
335
- 'content-type': 'text/plain'
336
- });
337
- response.end('SAP system information: client=100');
338
- });
339
- const targetOrigin = await listen(target);
340
- const service = createServer(async (request, response) => {
341
- if (request.url === '/uaa/oauth/token') {
342
- observed.destinationToken.push(request.headers.authorization);
343
- response.writeHead(200, { 'content-type': 'application/json' });
344
- response.end(JSON.stringify({ access_token: 'destination-api-token', expires_in: 300 }));
345
- } else if (request.url === '/destination-configuration/v1/instanceDestinations') {
346
- observed.destinationList.push(request.headers.authorization);
347
- response.writeHead(200, { 'content-type': 'application/json' });
348
- response.end(JSON.stringify([{
349
- Name: 'basic-onprem',
350
- Type: 'HTTP',
351
- URL: targetOrigin,
352
- Authentication: 'BasicAuthentication',
353
- ProxyType: 'OnPremise',
354
- Properties: [
355
- { Key: 'sap-client', Value: '100' },
356
- { Key: 'User', Value: 'sap-user' },
357
- { Key: 'Password', Value: 'sap-password' }
358
- ]
359
- }]));
360
- } else if (request.url === '/connectivity/oauth/token') {
361
- const chunks = [];
362
- for await (const chunk of request) chunks.push(chunk);
363
- observed.connectivityToken.push({
364
- authorization: request.headers.authorization,
365
- body: new URLSearchParams(Buffer.concat(chunks).toString())
366
- });
367
- response.writeHead(200, { 'content-type': 'application/json' });
368
- response.end(JSON.stringify({ access_token: 'application-proxy-token', expires_in: 300 }));
369
- } else {
370
- response.writeHead(404);
371
- response.end();
372
- }
373
- });
374
- const serviceOrigin = await listen(service);
375
- const connectivityProxy = createServer((request, response) => {
376
- observed.proxy.push({
377
- method: request.method,
378
- path: request.url,
379
- authorization: request.headers['proxy-authorization'],
380
- targetAuthorization: request.headers.authorization
381
- });
382
- if (request.headers['proxy-authorization'] !== 'Bearer application-proxy-token') {
383
- response.writeHead(403);
384
- response.end();
385
- request.resume();
386
- return;
387
- }
388
- const targetUrl = new URL(request.url);
389
- const headers = { ...request.headers, host: targetUrl.host };
390
- delete headers['proxy-authorization'];
391
- delete headers['sap-connectivity-scc-location_id'];
392
- delete headers.connection;
393
- const upstream = httpRequest({
394
- hostname: targetUrl.hostname,
395
- port: Number(targetUrl.port || 80),
396
- method: request.method,
397
- path: `${targetUrl.pathname}${targetUrl.search}`,
398
- headers
399
- }, upstreamResponse => {
400
- response.writeHead(upstreamResponse.statusCode || 502, upstreamResponse.headers);
401
- upstreamResponse.pipe(response);
402
- });
403
- upstream.on('error', () => {
404
- if (!response.headersSent) response.writeHead(502);
405
- response.end();
406
- });
407
- request.pipe(upstream);
408
- });
409
- connectivityProxy.on('connect', (request, socket) => {
410
- observed.connect.push(request.url);
411
- socket.end('HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n');
412
- });
413
- const connectivityProxyOrigin = await listen(connectivityProxy);
414
- const connectivityProxyPort = Number(new URL(connectivityProxyOrigin).port);
415
- const cfCalls = join(directory, 'cf-calls.jsonl');
416
- const cfEnv = await fakeCfExecutable(directory, serviceOrigin, cfCalls, {
417
- connectivityProxyPort,
418
- connectivityTokenUrl: `${serviceOrigin}/connectivity/oauth/token`
419
- });
420
- const credentialsPath = join(directory, 'sap-ai-dev-toolkit-credentials.json');
421
- await storeDestinationCredentials(credentialsPath, `cloud-foundry:cf:${SPACE_GUID}:${INSTANCE_GUID}:basic-onprem`, {
422
- user: 'override-user',
423
- password: 'override-password',
424
- mode: 'cf-connectivity'
425
- });
426
- const childLog = join(directory, 'vsp-children.jsonl');
427
- const servers = [service, target, connectivityProxy];
428
- t.after(async () => {
429
- for (const server of servers) server.closeAllConnections?.();
430
- await Promise.all(servers.map(server => new Promise(resolve => server.close(resolve))));
431
- await rm(directory, { recursive: true, force: true });
432
- });
433
-
434
- const result = await runLauncher({
435
- ...process.env,
436
- ...cfEnv,
437
- SAP_AI_DEV_TOOLKIT_BINARY: fakeVsp,
438
- SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
439
- SAP_AI_DEV_TOOLKIT_DESTINATION: 'basic-onprem',
440
- SAP_AI_DEV_TOOLKIT_CREDENTIALS_FILE: credentialsPath,
441
- BAS_CF_SPACE_GUID: SPACE_GUID,
442
- BAS_CF_DESTINATION_INSTANCE_GUID: INSTANCE_GUID,
443
- BAS_CF_DESTINATION_INSTANCE: INSTANCE_NAME,
444
- BAS_CF_DESTINATION_KEY: 'managed-destination-key',
445
- BAS_CF_DESTINATION_NAME: 'basic-onprem',
446
- BAS_CF_CONNECTIVITY_INSTANCE_GUID: 'runtime-connectivity-guid',
447
- BAS_CF_CONNECTIVITY_INSTANCE: 'runtime-connectivity-service',
448
- BAS_CF_CONNECTIVITY_KEY: 'managed-connectivity-key',
449
- H2O_URL: 'http://retained-for-checks.example',
450
- FAKE_LOG: childLog,
451
- FAKE_CF_ADT_TEST: 'true',
452
- HTTP_PROXY: '',
453
- HTTPS_PROXY: '',
454
- NO_PROXY: '127.0.0.1,localhost'
455
- }, [], [
456
- { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05' } },
457
- { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} },
458
- { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'basic-onprem__GetSystemInfo', arguments: {} } }
459
- ]);
460
-
461
- assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
462
- const responses = result.stdout.trim().split('\n').map(line => JSON.parse(line));
463
- assert.match(responses.at(-1).result.content[0].text, /SAP system information: client=100/);
464
- assert.deepEqual(observed.destinationToken, [`Basic ${Buffer.from('destination-client:destination-key-secret').toString('base64')}`]);
465
- assert.deepEqual(observed.destinationList, ['Bearer destination-api-token']);
466
- assert.deepEqual(observed.connectivityToken.map(item => [
467
- item.authorization,
468
- item.body.get('grant_type')
469
- ]), [[`Basic ${Buffer.from('connectivity-client:connectivity-key-secret').toString('base64')}`, 'client_credentials']]);
470
- assert.deepEqual(observed.proxy.map(item => [
471
- item.method,
472
- item.path,
473
- item.authorization,
474
- item.targetAuthorization
475
- ]), [[
476
- 'GET',
477
- `${targetOrigin}/sap/bc/adt/discovery`,
478
- 'Bearer application-proxy-token',
479
- `Basic ${Buffer.from('override-user:override-password').toString('base64')}`
480
- ]]);
481
- assert.deepEqual(observed.connect, []);
482
- assert.deepEqual(observed.target, [{
483
- path: '/sap/bc/adt/discovery',
484
- authorization: `Basic ${Buffer.from('override-user:override-password').toString('base64')}`
485
- }]);
486
- const events = (await readFile(childLog, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
487
- const initialized = events.find(event => event.event === 'initialize');
488
- assert.deepEqual([initialized.env.user, initialized.env.password], ['override-user', 'override-password']);
489
- assert.equal(initialized.argv.includes('--proxy-auth'), false);
490
- for (const secret of ['override-user', 'override-password', 'sap-password', 'connectivity-key-secret', 'application-proxy-token']) {
491
- assert.equal(`${result.stdout}\n${result.stderr}\n${JSON.stringify(initialized.argv)}`.includes(secret), false, `${secret} must not appear in output or argv`);
492
- }
493
- });
494
-
495
- test('CF list-destinations clears the CF name filter and remains BAS-only', async t => {
496
- const directory = await mkdtemp(join(tmpdir(), 'bas-cf-list-'));
497
- const callLog = join(directory, 'cf-calls.jsonl');
498
- const api = createServer((request, response) => {
499
- if (request.url === '/api/listDestinations') {
500
- response.writeHead(200, { 'content-type': 'application/json' });
501
- response.end(JSON.stringify([{ Name: 'bas-only', 'SAP-Client': '101' }]));
502
- return;
503
- }
504
- response.writeHead(404);
505
- response.end();
506
- });
507
- const origin = await listen(api);
508
- const cfEnv = await fakeCfExecutable(directory, origin, callLog);
509
- t.after(async () => {
510
- await new Promise(resolve => api.close(resolve));
511
- await rm(directory, { recursive: true, force: true });
512
- });
513
- const result = await runLauncher({
514
- ...process.env,
515
- ...cfEnv,
516
- SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE: 'cloud-foundry',
517
- SAP_AI_DEV_TOOLKIT_DESTINATION: 'cf-name-not-in-bas',
518
- H2O_URL: origin,
519
- HTTP_PROXY: '',
520
- HTTPS_PROXY: '',
521
- NO_PROXY: '127.0.0.1,localhost',
522
- SAP_AI_DEV_TOOLKIT_SKIP_PROBE: 'true'
523
- }, ['--list-destinations', '--json']);
524
- assert.equal(result.code, 0, result.stderr);
525
- assert.deepEqual(JSON.parse(result.stdout), [{ name: 'bas-only', client: '101', authentication: 'Unknown', probe: 'skipped' }]);
526
- await assert.rejects(() => readFile(callLog, 'utf8'), { code: 'ENOENT' });
527
- });
@@ -1,63 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
- import { createHash } from 'node:crypto';
5
- import { tmpdir } from 'node:os';
6
- import { dirname, join } from 'node:path';
7
- import { installUserCopilotAssets } from '../scripts/install-user-copilot-assets.mjs';
8
-
9
- async function writeAsset(root, path, content) {
10
- const filename = join(root, '.github', path);
11
- await mkdir(dirname(filename), { recursive: true });
12
- await writeFile(filename, content);
13
- }
14
-
15
- test('updates unchanged global assets and preserves user-edited customizations', async t => {
16
- const directory = await mkdtemp(join(tmpdir(), 'bas-copilot-assets-'));
17
- t.after(() => rm(directory, { recursive: true, force: true }));
18
- const root = join(directory, 'package');
19
- const home = join(directory, 'home');
20
- const agentSource = 'agents/abap-developer.agent.md';
21
- const skillSource = 'skills/example/SKILL.md';
22
- await writeAsset(root, agentSource, 'agent revision one');
23
- await writeAsset(root, skillSource, 'skill revision one');
24
-
25
- const initial = await installUserCopilotAssets({ home, root });
26
- assert.equal(initial.installed, 2);
27
- assert.deepEqual(initial.conflicts, []);
28
-
29
- await writeFile(join(home, '.copilot', 'agents', 'abap-developer.agent.md'), 'user customization');
30
- await writeAsset(root, agentSource, 'agent revision two');
31
- await writeAsset(root, skillSource, 'skill revision two');
32
- const update = await installUserCopilotAssets({ home, root });
33
-
34
- assert.equal(update.updated, 1);
35
- assert.deepEqual(update.conflicts, [join('agents', 'abap-developer.agent.md')]);
36
- assert.equal(await readFile(join(home, '.copilot', 'agents', 'abap-developer.agent.md'), 'utf8'), 'user customization');
37
- assert.equal(await readFile(join(home, '.copilot', 'skills', 'example', 'SKILL.md'), 'utf8'), 'skill revision two');
38
- });
39
-
40
- test('migrates the previous managed-file manifest without losing update ownership', async t => {
41
- const directory = await mkdtemp(join(tmpdir(), 'sap-ai-toolkit-assets-migration-'));
42
- t.after(() => rm(directory, { recursive: true, force: true }));
43
- const root = join(directory, 'package');
44
- const home = join(directory, 'home');
45
- const target = join(home, '.copilot', 'agents', 'abap-developer.agent.md');
46
- const oldManifest = join(home, '.copilot', '.bas-mcp-addon-assets.json');
47
- await writeAsset(root, 'agents/abap-developer.agent.md', 'managed revision one');
48
- await writeAsset(root, 'skills/example/SKILL.md', 'skill revision one');
49
- const first = await installUserCopilotAssets({ home, root });
50
- assert.equal(first.installed, 2);
51
-
52
- const managedContent = await readFile(target);
53
- const managedHash = createHash('sha256').update(managedContent).digest('hex');
54
- await rm(join(home, '.copilot', '.sap-ai-dev-toolkit-assets.json'));
55
- await writeFile(oldManifest, JSON.stringify({ version: 1, files: { [join('agents', 'abap-developer.agent.md')]: managedHash } }));
56
- await writeAsset(root, 'agents/abap-developer.agent.md', 'managed revision two');
57
-
58
- const migrated = await installUserCopilotAssets({ home, root });
59
- assert.equal(migrated.updated, 1);
60
- assert.equal(await readFile(target, 'utf8'), 'managed revision two');
61
- await assert.rejects(readFile(oldManifest), { code: 'ENOENT' });
62
- assert.equal(JSON.parse(await readFile(join(home, '.copilot', '.sap-ai-dev-toolkit-assets.json'), 'utf8')).version, 1);
63
- });