sap-ai-dev-toolkit 0.9.9920 → 0.9.9921
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/README.md +1 -1
- package/package.json +1 -1
- package/scripts/run-live-integration.mjs +7 -7
- package/src/setup.mjs +44 -32
package/README.md
CHANGED
|
@@ -234,7 +234,7 @@ The separate `integration-suite` stdio MCP server covers bounded discovery, auth
|
|
|
234
234
|
Interactive `sap-ai-dev --setup` asks **"Add the SAP Integration Suite tools?"** before writing MCP configuration, preserves existing entries, and shows configured identities without credential values. Enter defaults to yes when credentials exist, no otherwise. `--tools` includes the companion checkbox; the empty-landscape menu offers Integration Suite-only setup. Run **`sap-ai-dev --setup-integration`** to configure only this server.
|
|
235
235
|
|
|
236
236
|
- Detected credentials receive read-only capability checks. Core Cloud Foundry setup discovers `it-rt/api`, `apimanagement-apiportal/apiportal-apiaccess` and `apimanagement-devportal/devportal-apiaccess`; it can reuse/create the `sap-ai-dev-integration` key and, with explicit approval, dedicated `sap-ai-dev-integration-cpi`, `sap-ai-dev-integration-apim` or `sap-ai-dev-integration-devhub` instances. It pins plans/brokers visible to the targeted space, rejects ambiguous or other-organization plans, and does not grant entitlements.
|
|
237
|
-
- Core service keys and separately typed optional records can be pasted as hidden JSON or supplied by downloaded file path. The user-only `~/.config/sap-ai-dev-toolkit/integration-suite.json` stores only normalized credentials (directory 0700/file 0600), never `mcp.json`; saving a key that cannot sign in requires confirmation.
|
|
237
|
+
- Core service keys and separately typed optional records can be pasted as hidden JSON or supplied by downloaded file path; the paste option first asks which service the key is for (Cloud Integration, API Management, Developer Hub or an optional record). The user-only `~/.config/sap-ai-dev-toolkit/integration-suite.json` stores only normalized credentials (directory 0700/file 0600), never `mcp.json`; saving a key that cannot sign in requires confirmation.
|
|
238
238
|
- Optional discovery/setup accepts **existing credentials only**. It does not provision services, create optional service keys, activate capabilities, assign roles or retarget Cloud Foundry. Startup and capability probes never provision anything.
|
|
239
239
|
|
|
240
240
|
Core precedence remains per-side: complete explicit `CPI_URL/CPI_TOKEN_URL/CPI_CLIENT_ID/CPI_CLIENT_SECRET` (or `APIM_*` / `DEVHUB_*`) → matching `*_SERVICE_KEY` JSON → one unambiguous `VCAP_SERVICES` binding → saved setup key. `*_VCAP_SERVICE` and `*_BINDING` select custom labels/instances. Certificate-based core keys are rejected. Optional environment JSON overrides only its matching stored record; malformed optional input disables that adapter, not usable core clients.
|
package/package.json
CHANGED
|
@@ -30,7 +30,7 @@ const PROXY_URL = `ZAI_SmokeUrl${RUN}`;
|
|
|
30
30
|
const PROXY_ODATA = `ZAI_SmokeOData${RUN}`;
|
|
31
31
|
const PROXY_CPI = `ZAI_SmokeCpi${RUN}`;
|
|
32
32
|
const config = resolveIntegrationConfig(process.env, { stored: readStoredIntegrationKeys() });
|
|
33
|
-
const SECRETS = Object.values(config).filter(Boolean).map(side => side.clientSecret);
|
|
33
|
+
const SECRETS = Object.values(config).filter(Boolean).map(side => side.clientSecret).filter(Boolean);
|
|
34
34
|
const PRODUCT = `ZAI_SmokeProduct${RUN}`;
|
|
35
35
|
const KVM = `ZAI_SmokeKvm${RUN}`;
|
|
36
36
|
const CREDENTIAL = `ZAI_SMOKE_USER_${RUN}`;
|
|
@@ -99,7 +99,7 @@ async function invoke(url, { method = 'POST', body, headers = {}, auth = true, d
|
|
|
99
99
|
const text = await response.text();
|
|
100
100
|
let json;
|
|
101
101
|
try { json = JSON.parse(text); } catch { /* not JSON */ }
|
|
102
|
-
result = { status: response.status, text, json };
|
|
102
|
+
result = { status: response.status, headers: Object.fromEntries(response.headers), text, json };
|
|
103
103
|
if (done(result)) return result;
|
|
104
104
|
await sleep(5000);
|
|
105
105
|
}
|
|
@@ -562,7 +562,7 @@ await scenario('S41', 'runtime call to the template iFlow passes body and query
|
|
|
562
562
|
expect(/\/anything\/configured/.test(result.json.url), `receiver url ${result.json.url}`);
|
|
563
563
|
expect(result.json.args.probe === RUN, `query ${JSON.stringify(result.json.args)}`);
|
|
564
564
|
expect(JSON.parse(result.json.data).kind === 'direct', `body ${result.json.data}`);
|
|
565
|
-
ctx.directGuid = result.
|
|
565
|
+
ctx.directGuid = result.headers['sap_messageprocessinglogid'];
|
|
566
566
|
expect(ctx.directGuid, 'no MPL id header forwarded');
|
|
567
567
|
return `MPL ${ctx.directGuid}`;
|
|
568
568
|
});
|
|
@@ -579,7 +579,7 @@ await scenario('S43', 'APIM proxy in front of CPI forwards the caller bearer tok
|
|
|
579
579
|
const result = await invoke(proxy.runtime_url, { body: JSON.stringify({ via: 'apim-url-target', run: RUN }) });
|
|
580
580
|
expect(result.status === 200, `HTTP ${result.status}: ${result.text.slice(0, 200)}`);
|
|
581
581
|
expect(JSON.parse(result.json.data).via === 'apim-url-target', result.json.data);
|
|
582
|
-
ctx.apimGuid = result.
|
|
582
|
+
ctx.apimGuid = result.headers['sap_messageprocessinglogid'];
|
|
583
583
|
return `${proxy.runtime_url} → CPI → httpbin`;
|
|
584
584
|
});
|
|
585
585
|
|
|
@@ -611,8 +611,8 @@ await scenario('S46', 'iFlow with Unicode description, POST receiver and static
|
|
|
611
611
|
const { runtime } = await deployAndWait(IFLOW_POST);
|
|
612
612
|
expect(runtime.Status === 'STARTED', JSON.stringify(runtime));
|
|
613
613
|
const endpoints = ok(await call('cpi_list_service_endpoints', { iflow_id: IFLOW_POST }), 'endpoints').items;
|
|
614
|
-
ctx.postEndpoint = endpoints.flatMap(entry => entry.urls)[0] ?? ctx.cpiEndpoint
|
|
615
|
-
|
|
614
|
+
ctx.postEndpoint = endpoints.flatMap(entry => entry.urls)[0] ?? ctx.cpiEndpoint?.replace(`/http/ai/smoke${RUN}`, `/http/ai/post${RUN}`);
|
|
615
|
+
expect(ctx.postEndpoint, 'no POST endpoint; run with S18 in ONLY');
|
|
616
616
|
});
|
|
617
617
|
|
|
618
618
|
await scenario('S47', 'POST receiver gets method, static query and body', async () => {
|
|
@@ -914,7 +914,7 @@ await scenario('S79', 'full chain after recovery: client → APIM (provider) →
|
|
|
914
914
|
const proxy = ok(await call('apim_get_api_proxy', { name: PROXY_CPI_PROVIDER }), 'get');
|
|
915
915
|
const result = await invoke(proxy.runtime_url, { body: '{"final":true}', done: response => response.status === 200 && /restored/.test(response.json?.url || '') });
|
|
916
916
|
expect(result.status === 200 && JSON.parse(result.json.data).final === true, `HTTP ${result.status}`);
|
|
917
|
-
const detail = await waitForGuid(result.
|
|
917
|
+
const detail = await waitForGuid(result.headers['sap_messageprocessinglogid']);
|
|
918
918
|
expect(detail.log.Status === 'COMPLETED', detail.log.Status);
|
|
919
919
|
});
|
|
920
920
|
|
package/src/setup.mjs
CHANGED
|
@@ -292,18 +292,20 @@ export function integrationSuiteCredentialStatus(env = process.env) {
|
|
|
292
292
|
const config = resolveIntegrationConfig(env, { stored: readStoredIntegrationKeys(env) });
|
|
293
293
|
const keys = ['cpi', 'apim', 'devhub', ...OPTIONAL_COMPONENT_KEYS];
|
|
294
294
|
const side = (key, value) => {
|
|
295
|
-
if (!value) return 'not configured';
|
|
296
295
|
const host = value.url ? new URL(value.url).host : key === 'aem' ? 'api.solace.cloud' : 'unknown';
|
|
297
296
|
return `configured from ${value.source}${value.bindingName ? ` (${value.bindingName})` : ''}, host ${host}`;
|
|
298
297
|
};
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
const configured = keys.filter(key => config[key]);
|
|
299
|
+
const errors = (config.optional_config_errors ?? []).map(error => `${error.component.toUpperCase()}: invalid optional configuration (${error.source}); adapter disabled`);
|
|
300
|
+
if (!configured.length && !errors.length) return { found: false, config, lines: ['No Integration Suite credentials configured yet.'] };
|
|
301
|
+
return { found: configured.length > 0, config, lines: [
|
|
302
|
+
...(config.environment ? [`Environment label: ${config.environment}`] : []),
|
|
303
|
+
...configured.map(key => `${key.toUpperCase()}: ${side(key, config[key])}`),
|
|
304
|
+
...errors
|
|
303
305
|
] };
|
|
304
306
|
} catch (error) {
|
|
305
307
|
const message = String(error?.message || '');
|
|
306
|
-
if (/^Configure CPI/.test(message)) return { found: false, config: null, lines: ['No Integration Suite
|
|
308
|
+
if (/^Configure CPI/.test(message)) return { found: false, config: null, lines: ['No Integration Suite credentials configured yet.'] };
|
|
307
309
|
return { found: false, config: null, lines: [`Credentials found but not usable: ${message}`] };
|
|
308
310
|
}
|
|
309
311
|
}
|
|
@@ -330,7 +332,7 @@ function pastedServiceKey(side, value, env) {
|
|
|
330
332
|
}
|
|
331
333
|
|
|
332
334
|
async function printIntegrationVerification(config, { env, output, verifyIntegration }) {
|
|
333
|
-
const stop = startProgress('Checking
|
|
335
|
+
const stop = startProgress('Checking Integration Suite access', { output, env, label: 'Integration Suite' });
|
|
334
336
|
let report;
|
|
335
337
|
try { report = await verifyIntegration(config, { env }); }
|
|
336
338
|
finally { stop(); }
|
|
@@ -344,12 +346,12 @@ async function printIntegrationVerification(config, { env, output, verifyIntegra
|
|
|
344
346
|
}
|
|
345
347
|
const core = [report.cpi, report.apim, report.devhub].filter(side => side?.configured);
|
|
346
348
|
const denied = core.flatMap(side => side.checks ?? []).filter(check => check.result === 'forbidden').length;
|
|
347
|
-
const message = !core.length ? 'No core
|
|
348
|
-
: report.ok ?
|
|
349
|
-
: '
|
|
349
|
+
const message = !core.length ? 'No core service configured yet.'
|
|
350
|
+
: report.ok ? (denied ? `Connected; ${denied} area(s) were access-denied — the key may lack roles.` : 'Connection verified.')
|
|
351
|
+
: 'Sign-in or core read failed; see the lines above.';
|
|
350
352
|
print(output, formatStatus(message, report.ok && !denied ? 'success' : 'warning', output, 'Integration Suite'));
|
|
351
353
|
const optional = OPTIONAL_COMPONENT_KEYS.filter(side => report[side]?.configured);
|
|
352
|
-
if (optional.length) print(output, ` Optional
|
|
354
|
+
if (optional.length) print(output, ` Optional services: ${report.optional_completeness?.complete === true ? 'all verified' : 'not fully verified'} (separate from core access).`);
|
|
353
355
|
return report;
|
|
354
356
|
}
|
|
355
357
|
|
|
@@ -470,6 +472,26 @@ async function useCloudFoundrySpace(target, context) {
|
|
|
470
472
|
}
|
|
471
473
|
}
|
|
472
474
|
|
|
475
|
+
// The paste flow asks which service the key belongs to; APIM and Developer Hub keys share one OAuth shape,
|
|
476
|
+
// so the type is never guessed from the key itself.
|
|
477
|
+
async function chooseKeyType({ input, output, status }) {
|
|
478
|
+
return await selectPrompt({
|
|
479
|
+
message: colorText('Which service is this key for?', 'cyan', output),
|
|
480
|
+
defaultValue: ['CPI', 'APIM', 'DEVHUB'].find(side => !status.config?.[side.toLowerCase()]) ?? 'CPI',
|
|
481
|
+
choices: [
|
|
482
|
+
{ value: 'CPI', shortcut: 'i', name: 'Cloud Integration (CPI) — Process Integration Runtime, plan api' },
|
|
483
|
+
{ value: 'APIM', shortcut: 'a', name: 'API Management — plan apiportal-apiaccess' },
|
|
484
|
+
{ value: 'DEVHUB', shortcut: 'd', name: 'Developer Hub — plan devportal-apiaccess (AuthGroup.API.Admin)' },
|
|
485
|
+
...OPTIONAL_COMPONENT_KEYS.map(side => ({
|
|
486
|
+
value: side.toUpperCase(),
|
|
487
|
+
shortcut: { ctms: 't', ans: 'n', event_mesh: 'e', aem: 'm', aem_semp: 's', open_connectors: 'o', edge: 'g' }[side],
|
|
488
|
+
name: `Optional: ${OPTIONAL_INPUTS[side].label} (${OPTIONAL_INPUTS[side].variable}, existing credentials only)`
|
|
489
|
+
})),
|
|
490
|
+
{ value: '', shortcut: 'b', name: 'Back' }
|
|
491
|
+
]
|
|
492
|
+
}, { input, output });
|
|
493
|
+
}
|
|
494
|
+
|
|
473
495
|
// Verify the detected credentials online and let the user take CPI/APIM/DEVHUB keys from the logged-in CF space or
|
|
474
496
|
// paste them. A key is checked online first and saved to the user-only store (never to mcp.json); it is used
|
|
475
497
|
// when the environment has no key.
|
|
@@ -502,20 +524,13 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
|
|
|
502
524
|
: [];
|
|
503
525
|
const choice = await selectPrompt({
|
|
504
526
|
message: colorText('🔑 Integration Suite credentials', 'cyan', output),
|
|
505
|
-
defaultValue: healthy ? 'done' : (fixes[0]?.value ?? (target.available ? 'cf' : (status.found ? 'done' : '
|
|
527
|
+
defaultValue: healthy ? 'done' : (fixes[0]?.value ?? (target.available ? 'cf' : (status.found ? 'done' : 'paste'))),
|
|
506
528
|
choices: [
|
|
507
529
|
...fixes,
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
{
|
|
512
|
-
{ name: 'Paste a Developer Hub service key — API Management, Developer Hub, plan devportal-apiaccess (AuthGroup.API.Admin)', value: 'devhub', shortcut: 'd' },
|
|
513
|
-
...OPTIONAL_COMPONENT_KEYS.map(side => ({
|
|
514
|
-
name: `Paste existing ${OPTIONAL_INPUTS[side].label} configuration — ${OPTIONAL_INPUTS[side].variable} (no provisioning)`,
|
|
515
|
-
value: `optional-${side}`,
|
|
516
|
-
shortcut: { ctms: 't', ans: 'n', event_mesh: 'e', aem: 'm', aem_semp: 's', open_connectors: 'o', edge: 'g' }[side]
|
|
517
|
-
})),
|
|
518
|
-
...(status.found ? [{ name: 'Check the configured credentials online again', value: 'verify', shortcut: 'v' }] : [])
|
|
530
|
+
...(target.available ? [{ value: 'cf', shortcut: 'f', name: `Get keys from Cloud Foundry (${space})` }] : []),
|
|
531
|
+
{ value: 'paste', shortcut: 'p', name: 'Paste a service key (JSON or path to a .json file)' },
|
|
532
|
+
...(status.found ? [{ value: 'verify', shortcut: 'v', name: 'Check credentials again' }] : []),
|
|
533
|
+
{ value: 'done', shortcut: 'c', name: status.found ? (healthy ? 'Done' : 'Done (some checks did not pass)') : 'Skip for now (run sap-ai-dev --setup-integration later)' }
|
|
519
534
|
]
|
|
520
535
|
}, { input, output });
|
|
521
536
|
if (choice === 'done') return status;
|
|
@@ -527,8 +542,9 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
|
|
|
527
542
|
await createDedicatedInstance(choice.slice('create-'.length).toUpperCase(), context);
|
|
528
543
|
} else if (choice === 'cf') {
|
|
529
544
|
await useCloudFoundrySpace(target, context);
|
|
530
|
-
} else {
|
|
531
|
-
const side = (
|
|
545
|
+
} else if (choice === 'paste') {
|
|
546
|
+
const side = await chooseKeyType({ input, output, status });
|
|
547
|
+
if (!side) continue; // Back: show the menu again without refreshing status.
|
|
532
548
|
const pasted = await textPrompt({
|
|
533
549
|
message: `Paste existing ${side} credential JSON or the path to its .json file`,
|
|
534
550
|
secret: true,
|
|
@@ -550,16 +566,12 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
|
|
|
550
566
|
|
|
551
567
|
async function promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration = defaultVerifyIntegration, integrationCloudFoundry, ask = true }) {
|
|
552
568
|
print(output, '');
|
|
553
|
-
print(output, formatStatus('SAP Integration Suite tools
|
|
554
|
-
print(output, '
|
|
555
|
-
print(output, ' Writes require exact approval; private previews are not approval. Optional setup accepts existing credentials only, without provisioning. Agents: SAP Integration Architect, SAP Integration Developer.');
|
|
569
|
+
print(output, formatStatus('SAP Integration Suite tools', 'step', output, 'Integration Suite'));
|
|
570
|
+
print(output, ' Connects your AI agent to Cloud Integration (CPI), API Management and Developer Hub. Keys are checked read-only and stored only for you, never in mcp.json.');
|
|
556
571
|
if (alreadyConfigured) print(output, ' Already configured in this MCP config; keeping it and refreshing its version.');
|
|
557
572
|
const status = integrationSuiteCredentialStatus(env);
|
|
558
573
|
for (const line of status.lines) print(output, ` ${line}`);
|
|
559
|
-
if (!status.found)
|
|
560
|
-
print(output, ' Configure existing core or optional component credentials in the MCP host environment,');
|
|
561
|
-
print(output, ' or paste typed credential JSON in the next hidden step. Credentials are verified read-only and stored user-only, never in mcp.json.');
|
|
562
|
-
}
|
|
574
|
+
if (!status.found) print(output, ' No credentials found yet. Next you can get them from your Cloud Foundry space or paste a service key.');
|
|
563
575
|
if (ask && !alreadyConfigured) {
|
|
564
576
|
print(output, '');
|
|
565
577
|
const defaultYes = status.found;
|