sap-ai-dev-toolkit 0.9.9917 → 0.9.9920

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 (59) hide show
  1. package/.github/agents/sap-integration-architect.agent.md +32 -18
  2. package/.github/agents/sap-integration-developer.agent.md +28 -12
  3. package/.github/skills/integration-suite-delivery/SKILL.md +94 -37
  4. package/.github/skills/sap-integration-architect/SKILL.md +49 -25
  5. package/README.md +73 -14
  6. package/dist/checksums.txt +5 -5
  7. package/dist/vsp-darwin-arm64 +0 -0
  8. package/dist/vsp-darwin-x64 +0 -0
  9. package/dist/vsp-linux-arm64 +0 -0
  10. package/dist/vsp-linux-x64 +0 -0
  11. package/dist/vsp-win32-x64.exe +0 -0
  12. package/inventory.md +97 -57
  13. package/package.json +1 -1
  14. package/scripts/check-live-integration.mjs +151 -0
  15. package/scripts/install-local.mjs +11 -4
  16. package/scripts/run-live-integration.mjs +52 -60
  17. package/src/apim-bundle.mjs +416 -13
  18. package/src/apim-cleanup-tools.mjs +145 -0
  19. package/src/apim-content.mjs +157 -0
  20. package/src/apim-delete-tools.mjs +139 -0
  21. package/src/apim-documentation.mjs +162 -0
  22. package/src/apim-inspection.mjs +141 -0
  23. package/src/apim-monitoring.mjs +140 -0
  24. package/src/apim-policy-steps.mjs +31 -0
  25. package/src/apim-runtime.mjs +262 -0
  26. package/src/apim-tools.mjs +202 -109
  27. package/src/cf-integration.mjs +46 -7
  28. package/src/cpi-async-monitoring.mjs +177 -0
  29. package/src/cpi-content.mjs +92 -0
  30. package/src/cpi-monitoring.mjs +225 -0
  31. package/src/cpi-operational.mjs +129 -0
  32. package/src/cpi-release.mjs +273 -0
  33. package/src/cpi-security.mjs +48 -0
  34. package/src/cpi-tools.mjs +184 -319
  35. package/src/devhub-retirement.mjs +345 -0
  36. package/src/devhub-tools.mjs +177 -0
  37. package/src/edge-monitoring.mjs +101 -0
  38. package/src/iflow-bundle.mjs +295 -7
  39. package/src/integration-alerts.mjs +248 -0
  40. package/src/integration-analysis.mjs +488 -0
  41. package/src/integration-capabilities.mjs +181 -0
  42. package/src/integration-collections.mjs +203 -0
  43. package/src/integration-common.mjs +140 -35
  44. package/src/integration-config.mjs +20 -14
  45. package/src/integration-credentials.mjs +16 -10
  46. package/src/integration-http.mjs +255 -67
  47. package/src/integration-messaging.mjs +118 -0
  48. package/src/integration-optional-config.mjs +122 -0
  49. package/src/integration-optional-tools.mjs +153 -0
  50. package/src/integration-release-store.mjs +410 -0
  51. package/src/integration-server.mjs +34 -16
  52. package/src/integration-state.mjs +31 -0
  53. package/src/integration-tools.mjs +44 -8
  54. package/src/integration-transport.mjs +184 -0
  55. package/src/integration-validation.mjs +26 -0
  56. package/src/integration-verify.mjs +143 -62
  57. package/src/open-connectors-tools.mjs +143 -0
  58. package/src/setup.mjs +66 -44
  59. package/tools.md +193 -26
@@ -0,0 +1,488 @@
1
+ // Read-only, bounded static dependency evidence. No source, configuration values or tenant error bodies leave this module.
2
+ import { z } from 'zod';
3
+ import { READ_ONLY_ANNOTATIONS, entity, key } from './integration-common.mjs';
4
+ import { createCollectionPager, cursorSchema } from './integration-collections.mjs';
5
+ import { readCpiContent, CPI_BASE } from './cpi-content.mjs';
6
+ import { APIM_BASE, readApiProxy, exportApiProxyBundle } from './apim-content.mjs';
7
+ import { inspectApiProxyBundle } from './apim-inspection.mjs';
8
+ import { xmlLocate, xmlChildren, xmlText, xmlAttributes } from './apim-bundle.mjs';
9
+ import { parseProperties, strFromU8 } from './iflow-bundle.mjs';
10
+ import { canonicalDigest } from './integration-state.mjs';
11
+ import { readCertificateHealth, evaluateCertificate } from './cpi-security.mjs';
12
+ import { IntegrationError } from './integration-http.mjs';
13
+ import { validationEvidence } from './integration-validation.mjs';
14
+ import { capabilityCatalog } from './integration-capabilities.mjs';
15
+ import { validateNativeRules } from './integration-alerts.mjs';
16
+
17
+ const cpiId = z.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,99}$/);
18
+ const proxyId = z.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,99}$/);
19
+ const targetInputs = { cpi_iflow_ids: z.array(cpiId).min(1).max(20).optional(), apim_proxy_names: z.array(proxyId).min(1).max(20).optional() };
20
+ const targetsSchema = z.object(targetInputs).strict().refine(args => args.cpi_iflow_ids?.length || args.apim_proxy_names?.length, 'At least one explicit target is required');
21
+ const dependencySchema = z.object({ ...targetInputs, top: z.number().int().min(1).max(200).default(100), cursor: cursorSchema }).strict()
22
+ .refine(args => args.cpi_iflow_ids?.length || args.apim_proxy_names?.length, 'At least one explicit target is required');
23
+ const readinessSchema = z.object({ ...targetInputs, expected_versions: z.record(z.string().regex(/^[A-Za-z0-9_.-]{1,100}$/)).optional(), warning_days: z.number().int().min(1).max(365).default(30) }).strict()
24
+ .refine(args => args.cpi_iflow_ids?.length || args.apim_proxy_names?.length, 'At least one explicit target is required');
25
+ const local = element => element.name.split(':').at(-1);
26
+ const safeName = value => typeof value === 'string' && /^[A-Za-z0-9_.& -]{1,255}$/.test(value) ? value : null;
27
+ const safeVersion = value => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,100}$/.test(value) ? value : null;
28
+ const SOURCE_LIMIT = 128;
29
+ const MODEL_ELEMENTS = new Set(['definitions', 'collaboration', 'extensionElements', 'property', 'key', 'value', 'participant', 'messageFlow', 'process', 'startEvent', 'endEvent', 'serviceTask', 'sequenceFlow', 'incoming', 'outgoing', 'messageEventDefinition', 'BPMNDiagram', 'BPMNPlane', 'BPMNShape', 'BPMNEdge', 'Bounds', 'waypoint', 'BPMNLabel']);
30
+ const byteLength = value => Buffer.byteLength(JSON.stringify(value, null, 2));
31
+
32
+ function targets(args) {
33
+ return [...new Set(args.cpi_iflow_ids ?? [])].sort().map(id => ({ kind: 'cpi_iflow', id, key: `cpi_iflow:${id}` }))
34
+ .concat([...new Set(args.apim_proxy_names ?? [])].sort().map(id => ({ kind: 'apim_proxy', id, key: `apim_proxy:${id}` })));
35
+ }
36
+ function binding({ cpi, apim, summary = {} }) {
37
+ const host = side => {
38
+ try { return new URL(side?.origin).host; } catch { return null; }
39
+ };
40
+ return { environment: typeof summary.environment === 'string' ? summary.environment : 'default',
41
+ hosts: { cpi: host(cpi) ?? summary.cpi?.host ?? null, apim: host(apim) ?? summary.apim?.host ?? null } };
42
+ }
43
+ function failure(error) {
44
+ const status = Number(error?.status);
45
+ return { result: status === 401 ? 'unauthenticated' : status === 403 ? 'forbidden' : [404, 501].includes(status) ? 'unavailable' : 'error',
46
+ ...(Number.isInteger(status) && status >= 100 && status <= 599 ? { status } : {}),
47
+ ...(error?.code === 'INTEGRATION_DEADLINE' ? { stop_reason: 'deadline' } : {}) };
48
+ }
49
+ function unresolved(report, reason) {
50
+ if (!report.unresolved.some(item => item.reason === reason)) report.unresolved.push({ reason });
51
+ }
52
+ function addReference(report, kind, raw, required = true) {
53
+ const name = safeName(raw);
54
+ if (!name) {
55
+ if (required && raw === '') report.dependencies.push({ kind, name: null, required, existence: 'missing' });
56
+ else unresolved(report, `dynamic_or_unrecognized_${kind}`);
57
+ return;
58
+ }
59
+ if (!report.dependencies.some(item => item.kind === kind && item.name === name)) report.dependencies.push({ kind, name, required, existence: 'unknown' });
60
+ }
61
+
62
+ function inspectCpi(report, content) {
63
+ const defaults = content.files.get('src/main/resources/parameters.prop');
64
+ const parameters = new Map(Object.entries(defaults ? parseProperties(strFromU8(defaults)) : {}));
65
+ for (const entry of content.configurations) parameters.set(entry.ParameterKey, entry.ParameterValue);
66
+ const parameterNames = new Set(content.parameter_names);
67
+ const resolve = raw => {
68
+ if (typeof raw !== 'string') return undefined;
69
+ let missing = false;
70
+ const value = raw.replace(/\{\{([^{}]+)\}\}/g, (_, name) => {
71
+ parameterNames.add(name);
72
+ if (!parameters.has(name)) { missing = true; unresolved(report, 'missing_externalized_parameter'); return ''; }
73
+ return parameters.get(name);
74
+ });
75
+ if (missing) return undefined;
76
+ if (/\{\{|\}\}|\$\{|\{[^}]*\}/.test(value)) { unresolved(report, 'dynamic_configuration_value'); return undefined; }
77
+ return value.trim();
78
+ };
79
+ let models = 0;
80
+ let nodeCount = 0;
81
+ for (const [path, bytes] of content.files) {
82
+ if (/\.(?:groovy|gsh|js|py|jar|mmap|xsl|xslt)$/i.test(path) || /(?:scriptcollection|valuemapping|messagemapping)/i.test(path)) unresolved(report, 'custom_or_shared_content');
83
+ if (!path.endsWith('.iflw')) continue;
84
+ models += 1;
85
+ const xml = strFromU8(bytes);
86
+ const walk = (element, depth = 0) => {
87
+ if (++nodeCount > 20000 || depth > 100) throw new IntegrationError('CPI model exceeds the structural inspection limit');
88
+ if (!MODEL_ELEMENTS.has(local(element))) unresolved(report, 'unrecognized_model_element');
89
+ const children = xmlChildren(xml, element);
90
+ if (!children.length) resolve(xmlText(xml, element));
91
+ for (const value of Object.values(xmlAttributes(xml, element))) resolve(value);
92
+ if (local(element) === 'extensionElements') {
93
+ const properties = new Map();
94
+ for (const property of children.filter(child => local(child) === 'property')) {
95
+ const fields = xmlChildren(xml, property);
96
+ const field = name => fields.find(child => local(child) === name);
97
+ if (!field('key') || !field('value')) continue;
98
+ const name = xmlText(xml, field('key'));
99
+ if (properties.has(name)) unresolved(report, 'ambiguous_adapter_properties');
100
+ properties.set(name, xmlText(xml, field('value')));
101
+ }
102
+ const get = name => resolve(properties.get(name));
103
+ const credential = properties.has('credentialName') ? get('credentialName') : '';
104
+ if (properties.has('ComponentType')) {
105
+ const type = get('ComponentType');
106
+ if (!['HTTP', 'HTTPS'].includes(type)) unresolved(report, 'unrecognized_adapter');
107
+ if (['HTTP', 'HTTPS'].includes(type)) {
108
+ const auth = get('authenticationMethod');
109
+ if (auth === 'Basic') addReference(report, 'user_credential', credential);
110
+ else if (auth === 'OAuth2 Client Credentials') addReference(report, 'oauth2_client_credential', credential);
111
+ else if (auth === 'Client Certificate') addReference(report, 'certificate', credential);
112
+ else if (auth !== undefined && !['None', 'Principal Propagation'].includes(auth)) unresolved(report, 'unrecognized_authentication_method');
113
+ else if (properties.has('authenticationMethod') && auth === undefined) unresolved(report, 'dynamic_authentication_method');
114
+ else if (type === 'HTTP' && !properties.has('authenticationMethod')) unresolved(report, 'missing_authentication_method');
115
+ if (auth === 'Principal Propagation') unresolved(report, 'principal_propagation_native_configuration');
116
+ }
117
+ }
118
+ if (properties.has('clientCertificates')) {
119
+ const aliases = get('clientCertificates');
120
+ if (aliases === undefined) unresolved(report, 'dynamic_certificate');
121
+ else if (aliases) for (const alias of aliases.split(/[,;]/)) addReference(report, 'certificate', alias.trim());
122
+ }
123
+ const variant = get('cmdVariantUri');
124
+ if (variant && !/\/cname::(?:IFlowConfiguration|IntegrationProcess|MessageStartEvent|MessageEndEvent|ExternalCall|sap:HTTP|sap:HTTPS)\//.test(variant)) unresolved(report, 'unrecognized_or_shared_flow_element');
125
+ if ([...properties.keys()].some(name => /^(?:script|mapping|resource|credential|certificate|keystore|alias)/i.test(name) && !['credentialName', 'clientCertificates'].includes(name))) unresolved(report, 'unrecognized_dependency_property');
126
+ }
127
+ for (const child of children) walk(child, depth + 1);
128
+ };
129
+ walk(xmlLocate(xml, []));
130
+ }
131
+ if (!models) unresolved(report, 'missing_iflow_model');
132
+ report.parameter_names = [...parameterNames].filter(name => /^[A-Za-z_][A-Za-z0-9_.-]{0,199}$/.test(name)).sort();
133
+ if (report.parameter_names.length !== parameterNames.size) unresolved(report, 'unrecognized_parameter_identity');
134
+ }
135
+
136
+ async function collectPages(read) {
137
+ const items = [];
138
+ let cursor;
139
+ for (let index = 0; index < 10; index += 1) {
140
+ const page = await read(cursor);
141
+ items.push(...page.items);
142
+ if (page.complete) return { items, complete: true };
143
+ if (!page.next_cursor) return { items, complete: false, stop_reason: page.stop_reason };
144
+ cursor = page.next_cursor;
145
+ }
146
+ return { items, complete: false, stop_reason: 'source_limit' };
147
+ }
148
+
149
+ function inventoryReaders(clients, { deadline, warning_days, evaluated_at }) {
150
+ const cache = new Map();
151
+ return async function inventory(kind) {
152
+ if (!cache.has(kind)) cache.set(kind, (async () => {
153
+ try {
154
+ if (kind === 'certificate') return await collectPages(cursor => readCertificateHealth(clients.cpi, { top: 200, cursor, deadline, warning_days, evaluated_at }));
155
+ const cpi = ['user_credential', 'oauth2_client_credential'].includes(kind);
156
+ const client = cpi ? clients.cpi : clients.apim;
157
+ if (!client) return { items: [], complete: false, source_result: { result: 'unconfigured' } };
158
+ const root = cpi ? CPI_BASE : APIM_BASE;
159
+ const path = ({ user_credential: '/UserCredentials', oauth2_client_credential: '/OAuth2ClientCredentials', apim_certificate: '/CertificateStoreReferences' })[kind];
160
+ const pager = createCollectionPager(client, { root });
161
+ return await collectPages(cursor => pager.page({ path, mode: 'local', top: 200, cursor, deadline,
162
+ project: entry => cpi ? { name: safeName(entry.Name) } : { name: safeName(entry.name), alias: safeName(entry.alias), id: safeName(entry.id),
163
+ ...evaluateCertificate({ Alias: entry.alias ?? entry.name, Type: entry.type, ValidNotBefore: entry.ValidNotBefore, ValidNotAfter: entry.ValidNotAfter }, { warning_days, evaluated_at }) } }));
164
+ } catch (error) { return { items: [], complete: false, source_result: failure(error) }; }
165
+ })());
166
+ return cache.get(kind);
167
+ };
168
+ }
169
+
170
+ async function checkReferences(report, clients, inventory, options) {
171
+ for (const dependency of report.dependencies) {
172
+ if (dependency.existence === 'missing') continue;
173
+ try {
174
+ if (['provider', 'kvm'].includes(dependency.kind)) {
175
+ const set = dependency.kind === 'provider' ? 'APIProviders' : 'KeyMapEntries';
176
+ const response = await clients.apim.request('GET', `${APIM_BASE}/${set}(${key(dependency.name)})`, {
177
+ query: { $select: 'name' }, expect: [200, 404], withStatus: true, deadline: options.deadline
178
+ });
179
+ dependency.existence = response.status === 404 ? 'missing' : entity(response.data)?.name === dependency.name ? 'present' : 'unknown';
180
+ continue;
181
+ }
182
+ const kind = report.kind === 'apim_proxy' ? 'apim_certificate' : dependency.kind;
183
+ const source = await inventory(kind);
184
+ const inventoryComplete = source.complete && source.items.every(item => typeof (item.Alias ?? item.name ?? item.alias ?? item.id) === 'string');
185
+ const matches = source.items.filter(item => (item.Alias ?? item.name) === dependency.name || item.alias === dependency.name || item.id === dependency.name);
186
+ dependency.existence = matches.length === 1 ? 'present' : matches.length > 1 ? 'unknown' : inventoryComplete ? 'missing' : 'unknown';
187
+ if (!inventoryComplete) dependency.inventory_complete = false;
188
+ if (source.source_result) dependency.source_result = source.source_result;
189
+ if (source.stop_reason) dependency.stop_reason = source.stop_reason;
190
+ if (['certificate', 'keystore', 'truststore'].includes(dependency.kind) && matches.length === 1) {
191
+ const certificate = matches[0];
192
+ dependency.certificate = Object.fromEntries(['Type', 'KeyType', 'ValidNotBefore', 'ValidNotAfter', 'evaluated_at', 'days_remaining', 'validity'].filter(name => certificate[name] !== undefined).map(name => [name, certificate[name]]));
193
+ }
194
+ } catch (error) { dependency.existence = 'unknown'; dependency.source_result = failure(error); }
195
+ }
196
+ }
197
+
198
+ async function deployment(report, client, deadline) {
199
+ try {
200
+ if (report.kind === 'cpi_iflow') {
201
+ const result = await client.request('GET', `${CPI_BASE}/IntegrationRuntimeArtifacts(${key(report.id)})`, { expect: [200, 404], withStatus: true, deadline });
202
+ report.deployment = { result: 'ok', present: result.status === 200,
203
+ ...(result.status === 200 ? { state: safeName(result.data?.Status), version: safeVersion(result.data?.Version) } : {}) };
204
+ } else {
205
+ const proxy = await readApiProxy(client, report.id, { deadline });
206
+ report.deployment = { result: proxy ? 'ok' : 'unavailable', present: Boolean(proxy), state: safeName(proxy?.state), runtime_health: 'unverified' };
207
+ if (proxy) report.version = safeVersion(proxy.version);
208
+ }
209
+ } catch (error) { report.deployment = failure(error); }
210
+ }
211
+
212
+ async function inspectTarget(clients, target, inventory, options) {
213
+ const report = { ...target, version: null, parameter_names: [], dependencies: [], unresolved: [], sources: [], evidence: 'static_dependencies' };
214
+ const client = target.kind === 'cpi_iflow' ? clients.cpi : clients.apim;
215
+ if (!client) { report.sources.push({ source: 'content', result: 'unconfigured' }); return report; }
216
+ try {
217
+ if (target.kind === 'cpi_iflow') {
218
+ const content = await readCpiContent(client, target.id, 'active', options);
219
+ Object.assign(report, { version: safeVersion(content.resolved_version), selector: content.selector, bundle_sha256: content.bundle_sha256, state_sha256: content.state_sha256 });
220
+ inspectCpi(report, content);
221
+ report.validation = validationEvidence(client, { id: target.id, resolved_version: content.resolved_version, state_sha256: content.state_sha256 });
222
+ } else {
223
+ const files = await exportApiProxyBundle(client, target.id, options);
224
+ if (!files) { report.sources.push({ source: 'content', result: 'unavailable', status: 404 }); return report; }
225
+ const inspection = inspectApiProxyBundle(files, target.id);
226
+ Object.assign(report, { bundle_sha256: inspection.bundle_sha256, state_sha256: inspection.state_sha256,
227
+ policy_attachment: { policies: inspection.policies, attachments: inspection.attachments, unattached_policies: inspection.unattached_policies, unregistered_policy_files: inspection.unregistered_policy_files } });
228
+ for (const reference of inspection.references) addReference(report, reference.kind, reference.name);
229
+ for (const item of inspection.unresolved) unresolved(report, item.reason);
230
+ if (inspection.attachments.some(item => !item.registered) || inspection.policies.some(item => !item.document_present)) unresolved(report, 'missing_policy_registration_or_document');
231
+ }
232
+ report.sources.push({ source: 'content', result: 'ok' });
233
+ } catch (error) { report.sources.push({ source: 'content', ...failure(error) }); }
234
+ if (report.dependencies.length > SOURCE_LIMIT) { report.dependencies.length = SOURCE_LIMIT; unresolved(report, 'dependency_source_limit'); }
235
+ await checkReferences(report, clients, inventory, options);
236
+ await deployment(report, client, options.deadline);
237
+ return report;
238
+ }
239
+
240
+ function disposition(report, expected) {
241
+ const blockers = [];
242
+ const reviews = [];
243
+ if (expected !== undefined && report.version !== null && report.version !== expected) blockers.push('version_mismatch');
244
+ else if (expected !== undefined && report.version === null) reviews.push('version_unreadable');
245
+ if (report.sources.some(source => source.result !== 'ok')) reviews.push('content_unreadable');
246
+ if (report.unresolved.length) reviews.push('unresolved_dependencies');
247
+ if (!report.deployment || report.deployment.result !== 'ok') reviews.push('deployment_access_unverified');
248
+ if (report.deployment?.state === 'ERROR') reviews.push('existing_runtime_error');
249
+ if (report.kind === 'cpi_iflow') {
250
+ if (report.validation?.result === 'failed') blockers.push('latest_validation_failed');
251
+ else if (report.validation?.result !== 'passed') reviews.push('matching_validation_required');
252
+ }
253
+ for (const dependency of report.dependencies) {
254
+ if (dependency.required && dependency.existence === 'missing') blockers.push('missing_required_dependency');
255
+ if (dependency.existence === 'unknown') reviews.push('dependency_unverified');
256
+ const validity = dependency.certificate?.validity;
257
+ if (['expired', 'not_yet_valid'].includes(validity)) blockers.push('referenced_certificate_invalid');
258
+ if (['certificate', 'keystore', 'truststore'].includes(dependency.kind) && dependency.existence === 'present' && (!validity || validity === 'unknown')) reviews.push('certificate_validity_unknown');
259
+ if (validity === 'expiring') reviews.push('referenced_certificate_expiring');
260
+ }
261
+ return { readiness: blockers.length ? 'blocked' : reviews.length ? 'needs_review' : 'ready', blockers: [...new Set(blockers)], reviews: [...new Set(reviews)] };
262
+ }
263
+
264
+ function boundReport(report, maxBytes) {
265
+ if (byteLength(report) <= maxBytes) return report;
266
+ const bounded = { ...report, dependencies: [], parameter_names: [], unresolved: [{ reason: 'output_limit' }], evidence_complete: false };
267
+ delete bounded.policy_attachment;
268
+ return bounded;
269
+ }
270
+
271
+ export async function inspectDependencies(clients, input, { deadline = Date.now() + 60000 } = {}) {
272
+ const args = dependencySchema.parse(input);
273
+ const selected = targets(args);
274
+ const client = clients.cpi ?? clients.apim;
275
+ if (!client) throw new IntegrationError('No integration component is configured');
276
+ const identity = canonicalDigest(selected);
277
+ let rows = [];
278
+ const pager = createCollectionPager(client, { root: '/integration-analysis', request: async () => rows });
279
+ const context = args.cursor ? pager.cursorContext(args.cursor, { scope: 'dependencies' }) : { targets: identity, evaluated_at: new Date().toISOString() };
280
+ if (context.targets !== identity) throw new IntegrationError('Collection cursor targets changed; restart the listing');
281
+ const options = { deadline, warning_days: 30, evaluated_at: context.evaluated_at };
282
+ const inventory = inventoryReaders(clients, options);
283
+ for (const target of selected) rows.push(boundReport(await inspectTarget(clients, target, inventory, options), 20000));
284
+ const page = await pager.page({ path: '/Dependencies', mode: 'local', raw: rows, top: args.top, cursor: args.cursor, scope: 'dependencies', context, deadline, maxBytes: 88 * 1024 });
285
+ return { ...binding(clients), ...page, evidence: 'static_dependencies', graph_completeness: 'not_a_complete_dependency_graph' };
286
+ }
287
+
288
+ export async function checkReleaseReadiness(clients, input, { deadline = Date.now() + 60000 } = {}) {
289
+ const args = readinessSchema.parse(input);
290
+ targetsSchema.parse({ cpi_iflow_ids: args.cpi_iflow_ids, apim_proxy_names: args.apim_proxy_names });
291
+ const selected = targets(args);
292
+ for (const name of Object.keys(args.expected_versions ?? {})) if (!selected.some(target => target.key === name)) throw new IntegrationError('expected_versions contains an unselected target');
293
+ const options = { deadline, warning_days: args.warning_days, evaluated_at: new Date().toISOString() };
294
+ const inventory = inventoryReaders(clients, options);
295
+ const items = [];
296
+ let used = 0;
297
+ for (const target of selected) {
298
+ const report = await inspectTarget(clients, target, inventory, options);
299
+ Object.assign(report, disposition(report, args.expected_versions?.[target.key]));
300
+ if (args.expected_versions?.[target.key] !== undefined) report.expected_version = args.expected_versions[target.key];
301
+ const bounded = boundReport(report, Math.min(16000, Math.max(1200, 78 * 1024 - used - (selected.length - items.length) * 1200)));
302
+ // Omitted evidence must never acquire a stronger readiness claim.
303
+ if (bounded.evidence_complete === false && bounded.readiness === 'ready') { bounded.readiness = 'needs_review'; bounded.reviews = [...bounded.reviews, 'output_limit']; }
304
+ items.push(bounded);
305
+ used += byteLength(bounded);
306
+ }
307
+ return { ...binding(clients), evaluated_at: options.evaluated_at, readiness: items.some(item => item.readiness === 'blocked') ? 'blocked' : items.some(item => item.readiness === 'needs_review') ? 'needs_review' : 'ready',
308
+ readiness_scope: 'static_dependencies_management_access_and_matching_iflow_validation', validation: 'required_for_cpi_iflows', runtime_health: 'unverified', items,
309
+ complete: items.every(item => item.sources.every(source => source.result === 'ok') && item.deployment?.result === 'ok' && item.dependencies.every(dependency => dependency.existence !== 'unknown') && !item.unresolved.length && item.evidence_complete !== false && (item.kind !== 'cpi_iflow' || ['passed', 'failed', 'unknown'].includes(item.validation?.result))) };
310
+ }
311
+
312
+ export function registerIntegrationAnalysisTools(register, { cpi, apim, summary } = {}) {
313
+ const clients = { cpi, apim, summary };
314
+ register('integration_inspect_dependencies', 'Inspect explicit integration dependencies',
315
+ 'Privately read fresh bundles and configurations; return bounded static alias/provider/KVM/certificate evidence, identity digests and parameter names only. Not a complete dependency graph.', dependencySchema, READ_ONLY_ANNOTATIONS,
316
+ args => inspectDependencies(clients, args));
317
+ register('integration_check_release_readiness', 'Check static integration release readiness',
318
+ 'Read-only explicit-target dependency/version/management checks and latest matching iFlow validation evidence. Missing required aliases, referenced invalid certificates and latest failed validation block; missing/stale/unknown validation and dynamic/unreadable evidence require review. Never initiates validation or deployment.', readinessSchema, READ_ONLY_ANNOTATIONS,
319
+ args => checkReleaseReadiness(clients, args));
320
+ }
321
+
322
+ const monitoringSchema = z.object({
323
+ components: z.array(z.string().regex(/^[a-z][a-z0-9_]{0,63}$/)).min(1).max(20).optional(),
324
+ node_names: z.array(z.string().min(1).max(256).refine(value => !/[\u0000-\u001f\u007f]/.test(value))).min(1).max(20).optional()
325
+ }).strict();
326
+ const MONITORING_CHECKS = Object.freeze({
327
+ cpi: ['Deployed artifacts', 'Message logs', 'MPL counts', 'Keystore validity', 'Data stores', 'JMS queues', 'Queue metadata'],
328
+ apim: ['API proxies', 'API analytics'],
329
+ ctms: ['Transport nodes'],
330
+ ans: ['Alert subscription metadata', 'Alert condition metadata', 'Alert action metadata'],
331
+ event_mesh: ['Named queue management'],
332
+ aem: ['GA event broker services'],
333
+ aem_semp: ['VPN queue metadata'],
334
+ open_connectors: ['Connector instance identities'],
335
+ edge: ['Local OData metadata']
336
+ });
337
+ const CTMS_EVENTS = Object.freeze(['TmsImportStarted', 'TmsImportFinished', 'TmsTransportRequestAdded', 'TmsNodeImportJobDeactivated', 'TmsStorageQuotaUsage']);
338
+ const ACCESS_RESULTS = new Set(['ok', 'unauthenticated', 'forbidden', 'unavailable', 'untested', 'error']);
339
+ const ALM_PERMISSIONS = Object.freeze(['MonitoringDataRead', 'MonitoringArtifactsDeploy', 'HealthCheckMonitoringDataRead']);
340
+
341
+ function monitoringSources(component, report) {
342
+ return MONITORING_CHECKS[component].map(area => {
343
+ const check = report?.checks?.find(entry => entry.area === area);
344
+ return {
345
+ component, area, method: 'GET',
346
+ result: !report?.configured ? 'unconfigured' : ACCESS_RESULTS.has(check?.result) ? check.result : 'missing_evidence',
347
+ status: Number.isInteger(check?.status) && check.status >= 100 && check.status <= 599 ? check.status : null,
348
+ ...(check?.stop_reason === 'deadline' || check?.source?.stop_reason === 'deadline' ? { stop_reason: 'deadline' } : {})
349
+ };
350
+ });
351
+ }
352
+
353
+ function monitoringHandoffs(component, catalog) {
354
+ if (component === 'cpi' || component === 'cloud_alm') return [{
355
+ owner: 'Cloud ALM administrator',
356
+ prerequisites: ['Dedicated CPI API credential for Cloud ALM; do not reuse or grant permissions to the developer credential',
357
+ ...ALM_PERMISSIONS.map(permission => `${permission}: separate setup requirement; not granted by this tool`)],
358
+ steps: ['Configure the CPI endpoint in Cloud ALM Landscape Management',
359
+ 'Enable Integration & Exception Monitoring collection and required categories',
360
+ 'Enable Health Monitoring for CPI performance, certificates, JMS and resources',
361
+ 'Configure native event thresholds and notification recipients',
362
+ 'Observe incoming collection data and an explicitly approved synthetic notification delivery'],
363
+ safety: 'Accessible CPI management APIs do not establish continuous collection. A CPI processing failure is not automatically an ANS event.',
364
+ post_action_verification: ['Administrator supplies collection and notification-delivery evidence for this bound CPI tenant']
365
+ }];
366
+ if (component === 'ctms') return [{
367
+ owner: 'cTMS and ANS administrators',
368
+ prerequisites: ['Existing cTMS and ANS in the intended subaccount; exact node identities and approved notification action',
369
+ 'Common-subaccount identity remains unverified by these independent credentials'],
370
+ steps: ['Use the current trusted internal ANS integration for the subaccount',
371
+ 'Select intentional eventType conditions for the five supported cTMS event types',
372
+ 'Constrain subscriptions with tags.nodeName or tags.nodeId for the intended nodes',
373
+ 'Enable the intended subscriptions and actions; inspect native notification status',
374
+ 'Observe an explicitly approved synthetic event and its delivered notification'],
375
+ safety: 'Do not create the obsolete ALERT_NOTIFICATION_SERVICE destination; SAP schedules its removal in Q4 2026. Node/rule reads do not prove event collection or delivery.',
376
+ post_action_verification: ['Inspect exact event/node rule scope and administrator-provided collection/delivery evidence']
377
+ }];
378
+ if (component === 'apim') return [{
379
+ owner: 'API Management and alerting administrators',
380
+ prerequisites: ['Exact proxy identities and native anomaly-event/notification configuration'],
381
+ steps: ['Inspect analytics source coverage and watermark separately from native anomaly events',
382
+ 'Configure native anomaly-event collection, thresholds and notification actions for the intended proxies',
383
+ 'Observe event collection and an explicitly approved synthetic notification delivery'],
384
+ safety: 'Analytics polling is on-demand management evidence, not verified anomaly-event collection or notification delivery.',
385
+ post_action_verification: ['Administrator confirms native anomaly-event collection and delivered notification for the selected tenant']
386
+ }];
387
+ return catalog[component]?.native_handoff ?? [];
388
+ }
389
+
390
+ /** On-demand source access and native setup evidence; never a continuous-monitoring sign-off. */
391
+ export async function monitoringReadiness(clients, input = {}) {
392
+ const args = monitoringSchema.parse(input);
393
+ const { cpi, apim, ans, summary = {}, verify } = clients;
394
+ const configured = component => Boolean(({ cpi, apim, ans })[component]) || summary[component]?.configured === true;
395
+ const selected = [...new Set(args.components ?? Object.keys(MONITORING_CHECKS).filter(configured))];
396
+ const evaluated_at = new Date().toISOString();
397
+ const deadline = Date.now() + 60000;
398
+ const needsRules = selected.some(component => component === 'ans' || component === 'ctms');
399
+ const [verification, nativeRules] = await Promise.all([
400
+ (async () => {
401
+ try { return typeof verify === 'function' ? await verify() : {}; }
402
+ catch (error) { return { source_failure: failure(error) }; }
403
+ })(),
404
+ (async () => {
405
+ if (!needsRules || !ans) return null;
406
+ try { return await validateNativeRules(ans, { ...(args.node_names ? { node_names: args.node_names } : {}) }, { deadline }); }
407
+ catch (error) {
408
+ return { complete: false, outcome: 'blocked', collection: 'needs_admin_confirmation', delivery: 'needs_admin_confirmation',
409
+ findings: [{ code: 'rule_source_failed', severity: 'blocked', source_result: failure(error) }], items: [] };
410
+ }
411
+ })()
412
+ ]);
413
+ const catalog = capabilityCatalog({ summary, verification });
414
+ const ruleSourceFailures = [...(nativeRules?.findings ?? []), ...(nativeRules?.items ?? []).flatMap(item => item.findings ?? [])]
415
+ .filter(finding => finding.source_result && finding.source_result.result !== 'ok');
416
+ const hosts = {};
417
+ for (const component of [...new Set([...selected.map(name => name === 'cloud_alm' ? 'cpi' : name), ...(needsRules ? ['ans'] : [])])]) {
418
+ if (!Object.hasOwn(MONITORING_CHECKS, component)) continue;
419
+ const client = ({ cpi, apim, ans })[component];
420
+ try { hosts[component] = new URL(client?.origin ?? `https://${summary[component]?.host}`).host; }
421
+ catch { hosts[component] = null; }
422
+ if (!client?.origin && !summary[component]?.host) hosts[component] = null;
423
+ }
424
+ const items = selected.map(component => {
425
+ const sourceComponent = component === 'cloud_alm' ? 'cpi' : component;
426
+ if (!Object.hasOwn(MONITORING_CHECKS, sourceComponent)) return {
427
+ component, configuration: 'unsupported', readiness: 'blocked', continuous_monitoring: 'unverified',
428
+ sources: [], findings: [{ code: 'unsupported_component', severity: 'blocked' }],
429
+ native_handoff: catalog[component]?.native_handoff ?? [],
430
+ next_steps: [`Select a supported monitoring component: ${[...Object.keys(MONITORING_CHECKS), 'cloud_alm'].join(', ')}`]
431
+ };
432
+ const report = verification[sourceComponent];
433
+ const isConfigured = configured(sourceComponent) || report?.configured === true;
434
+ const sources = monitoringSources(sourceComponent, { ...report, configured: isConfigured });
435
+ if (verification.source_failure) for (const source of sources) Object.assign(source, verification.source_failure);
436
+ if (component === 'ctms') sources.push(...monitoringSources('ans', { ...verification.ans, configured: Boolean(ans) }));
437
+ const findings = sources.filter(source => source.result !== 'ok').map(source => ({
438
+ code: source.result === 'untested' ? 'management_source_untested' : 'required_management_source_unavailable',
439
+ severity: source.result === 'untested' ? 'needs_review' : 'blocked', component: source.component, area: source.area, result: source.result
440
+ }));
441
+ if (component === 'ctms' || component === 'ans') {
442
+ if (!ans) findings.push({ code: 'ans_configuration_required', severity: 'blocked' });
443
+ if (ruleSourceFailures.length) findings.push({ code: 'required_rule_source_unavailable', severity: 'blocked' });
444
+ if (nativeRules?.outcome === 'blocked') findings.push({ code: 'native_rules_blocked', severity: 'blocked' });
445
+ else if (nativeRules && (nativeRules.outcome !== 'rules_validated' || !nativeRules.complete)) findings.push({ code: 'native_rules_require_review', severity: 'needs_review' });
446
+ }
447
+ if (component === 'ctms' && nativeRules) {
448
+ const scoped = (nativeRules.items ?? []).filter(item => item.outcome === 'rules_validated' && item.scope?.verified);
449
+ const coveredEvents = new Set(scoped.flatMap(item => item.scope.event_types ?? []));
450
+ const missingEvents = CTMS_EVENTS.filter(event => !coveredEvents.has(event));
451
+ if (missingEvents.length) findings.push({ code: 'ctms_event_coverage_unconfirmed', severity: 'needs_review', event_types: missingEvents });
452
+ if (!args.node_names?.length) findings.push({ code: 'intended_ctms_nodes_not_supplied', severity: 'needs_review' });
453
+ else for (const node of args.node_names) {
454
+ if (!scoped.some(item => item.scope.node_names?.includes(node))) findings.push({ code: 'ctms_node_coverage_unconfirmed', severity: 'needs_review', node_name: node });
455
+ }
456
+ }
457
+ return {
458
+ component, configuration: isConfigured ? 'configured' : 'absent',
459
+ readiness: findings.some(finding => finding.severity === 'blocked') ? 'blocked' : 'needs_admin_confirmation',
460
+ continuous_monitoring: 'unverified', collection: 'needs_admin_confirmation', delivery: 'needs_admin_confirmation',
461
+ activation: 'unknown', entitlement: 'unknown', sources, findings,
462
+ ...(component === 'apim' ? { analytics_polling: sources.find(source => source.area === 'API analytics')?.result ?? 'missing_evidence', anomaly_events: 'needs_admin_confirmation' } : {}),
463
+ ...(component === 'ctms' ? { event_types: CTMS_EVENTS, node_names: args.node_names ?? [], integration: 'trusted_internal_ans' } : {}),
464
+ ...(component === 'cpi' || component === 'cloud_alm' ? { monitoring_permissions: ALM_PERMISSIONS, permission_effect: 'setup_requirements_only_no_grants', cloud_alm_client: 'not_configured' } : {}),
465
+ setup_requirements: catalog[component]?.setup_requirements ?? [],
466
+ source_links: [...new Set([...(catalog[component]?.source_links ?? []), ...(component === 'cpi' ? catalog.cloud_alm.source_links : [])])],
467
+ native_handoff: monitoringHandoffs(component, catalog)
468
+ };
469
+ });
470
+ return {
471
+ environment: typeof summary.environment === 'string' ? summary.environment : 'default', hosts, evaluated_at,
472
+ readiness: !items.length || items.some(item => item.readiness === 'blocked') ? 'blocked' : 'needs_admin_confirmation',
473
+ readiness_scope: 'accessible_on_demand_management_and_native_monitoring_setup',
474
+ continuous_monitoring: 'unverified', collection: 'needs_admin_confirmation', delivery: 'needs_admin_confirmation',
475
+ common_subaccount: 'unverified', complete: false,
476
+ evidence_complete: items.length > 0 && items.every(item => item.sources.length > 0 && item.sources.every(source => source.result === 'ok')) && (!needsRules || nativeRules?.complete === true),
477
+ findings: items.length ? [] : [{ code: 'no_configured_monitoring_components', severity: 'blocked' }],
478
+ items, ...(nativeRules ? { native_rules: nativeRules } : {}),
479
+ verification_scope: 'source_access_only_not_health_or_continuous_collection',
480
+ runtime_behavior: 'on_demand_only_no_daemon_scheduler_or_provisioning'
481
+ };
482
+ }
483
+
484
+ export function registerMonitoringReadinessTools(register, { cpi, apim, ans, summary, verify } = {}) {
485
+ register('integration_monitoring_readiness', 'Inspect native monitoring readiness',
486
+ 'Read-only management/rule evidence plus administrator-run native setup requirements. API access does not verify continuous collection, anomaly events or notification delivery; missing or denied required sources block readiness. No daemon, provisioning or permission grants.',
487
+ monitoringSchema, READ_ONLY_ANNOTATIONS, args => monitoringReadiness({ cpi, apim, ans, summary, verify }, args));
488
+ }