arc-1 0.4.4 → 0.6.0
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 +6 -5
- package/dist/adt/client.d.ts +11 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +51 -1
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/codeintel.js +1 -1
- package/dist/adt/codeintel.js.map +1 -1
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +1 -7
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/features.d.ts +29 -1
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +114 -2
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/http.d.ts +24 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +87 -28
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/oauth.d.ts +19 -2
- package/dist/adt/oauth.d.ts.map +1 -1
- package/dist/adt/oauth.js +78 -28
- package/dist/adt/oauth.js.map +1 -1
- package/dist/adt/safety.d.ts +15 -7
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +49 -45
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/types.d.ts +33 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +10 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +47 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/aff/schemas/bdef-v1.json +62 -0
- package/dist/aff/schemas/clas-v1.json +276 -0
- package/dist/aff/schemas/ddls-v1.json +144 -0
- package/dist/aff/schemas/intf-v1.json +243 -0
- package/dist/aff/schemas/prog-v1.json +133 -0
- package/dist/aff/schemas/srvb-v1.json +115 -0
- package/dist/aff/schemas/srvd-v1.json +108 -0
- package/dist/aff/validator.d.ts +14 -0
- package/dist/aff/validator.d.ts.map +1 -0
- package/dist/aff/validator.js +83 -0
- package/dist/aff/validator.js.map +1 -0
- package/dist/handlers/hyperfocused.d.ts +1 -0
- package/dist/handlers/hyperfocused.d.ts.map +1 -1
- package/dist/handlers/hyperfocused.js +7 -6
- package/dist/handlers/hyperfocused.js.map +1 -1
- package/dist/handlers/intent.d.ts +17 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +369 -27
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +296 -0
- package/dist/handlers/schemas.d.ts.map +1 -0
- package/dist/handlers/schemas.js +250 -0
- package/dist/handlers/schemas.js.map +1 -0
- package/dist/handlers/tools.d.ts +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +111 -42
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/zod-errors.d.ts +20 -0
- package/dist/handlers/zod-errors.d.ts.map +1 -0
- package/dist/handlers/zod-errors.js +43 -0
- package/dist/handlers/zod-errors.js.map +1 -0
- package/dist/server/config.d.ts +26 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +156 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts +8 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +134 -71
- package/dist/server/http.js.map +1 -1
- package/dist/server/server.d.ts +13 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +82 -9
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +8 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -2
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +11 -1
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +127 -9
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +4 -3
package/dist/handlers/intent.js
CHANGED
|
@@ -13,10 +13,11 @@ import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '.
|
|
|
13
13
|
import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
|
|
14
14
|
import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
|
|
15
15
|
import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
|
|
16
|
-
import { AdtApiError, AdtNetworkError, AdtSafetyError } from '../adt/errors.js';
|
|
17
|
-
import { mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
18
|
-
import { isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
16
|
+
import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
|
|
17
|
+
import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
18
|
+
import { checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
19
19
|
import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
|
|
20
|
+
import { validateAffHeader } from '../aff/validator.js';
|
|
20
21
|
import { extractCdsElements } from '../context/cds-deps.js';
|
|
21
22
|
import { compressCdsContext, compressContext } from '../context/compressor.js';
|
|
22
23
|
import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
|
|
@@ -26,6 +27,8 @@ import { sanitizeArgs } from '../server/audit.js';
|
|
|
26
27
|
import { generateRequestId, requestContext } from '../server/context.js';
|
|
27
28
|
import { logger } from '../server/logger.js';
|
|
28
29
|
import { expandHyperfocusedArgs, getHyperfocusedScope } from './hyperfocused.js';
|
|
30
|
+
import { getToolSchema } from './schemas.js';
|
|
31
|
+
import { formatZodError } from './zod-errors.js';
|
|
29
32
|
/**
|
|
30
33
|
* Scope required for each tool.
|
|
31
34
|
*
|
|
@@ -39,7 +42,7 @@ import { expandHyperfocusedArgs, getHyperfocusedScope } from './hyperfocused.js'
|
|
|
39
42
|
export const TOOL_SCOPES = {
|
|
40
43
|
SAPRead: 'read',
|
|
41
44
|
SAPSearch: 'read',
|
|
42
|
-
SAPQuery: '
|
|
45
|
+
SAPQuery: 'sql',
|
|
43
46
|
SAPNavigate: 'read',
|
|
44
47
|
SAPContext: 'read',
|
|
45
48
|
SAPLint: 'read',
|
|
@@ -47,8 +50,24 @@ export const TOOL_SCOPES = {
|
|
|
47
50
|
SAPWrite: 'write',
|
|
48
51
|
SAPActivate: 'write',
|
|
49
52
|
SAPManage: 'write',
|
|
50
|
-
SAPTransport: '
|
|
53
|
+
SAPTransport: 'write',
|
|
51
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Check if authInfo has the required scope, respecting implied scopes:
|
|
57
|
+
* - `write` implies `read`
|
|
58
|
+
* - `sql` implies `data`
|
|
59
|
+
*/
|
|
60
|
+
export function hasRequiredScope(authInfo, requiredScope) {
|
|
61
|
+
const scopes = authInfo.scopes;
|
|
62
|
+
if (scopes.includes(requiredScope))
|
|
63
|
+
return true;
|
|
64
|
+
// Implied scopes
|
|
65
|
+
if (requiredScope === 'read' && scopes.includes('write'))
|
|
66
|
+
return true;
|
|
67
|
+
if (requiredScope === 'data' && scopes.includes('sql'))
|
|
68
|
+
return true;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
52
71
|
function textResult(text) {
|
|
53
72
|
return { content: [{ type: 'text', text }] };
|
|
54
73
|
}
|
|
@@ -92,7 +111,7 @@ function classifyError(err) {
|
|
|
92
111
|
* all tools are allowed (backward compatibility).
|
|
93
112
|
* @param server - MCP Server instance for elicitation support.
|
|
94
113
|
*/
|
|
95
|
-
export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer) {
|
|
114
|
+
export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient) {
|
|
96
115
|
const reqId = generateRequestId();
|
|
97
116
|
const start = Date.now();
|
|
98
117
|
// Build user context for audit logging
|
|
@@ -112,7 +131,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
112
131
|
// Scope enforcement — only when authInfo is present (XSUAA/OIDC mode)
|
|
113
132
|
if (authInfo) {
|
|
114
133
|
const requiredScope = TOOL_SCOPES[toolName];
|
|
115
|
-
if (requiredScope && !authInfo
|
|
134
|
+
if (requiredScope && !hasRequiredScope(authInfo, requiredScope)) {
|
|
116
135
|
logger.emitAudit({
|
|
117
136
|
timestamp: new Date().toISOString(),
|
|
118
137
|
level: 'warn',
|
|
@@ -127,6 +146,29 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
127
146
|
return errorResult(`Insufficient scope: '${requiredScope}' required for ${toolName}. Your scopes: [${authInfo.scopes.join(', ')}]`);
|
|
128
147
|
}
|
|
129
148
|
}
|
|
149
|
+
// Validate tool arguments with Zod schema
|
|
150
|
+
const isBtp = config.systemType === 'btp';
|
|
151
|
+
// Always use the full search schema for validation — the handler checks text search availability
|
|
152
|
+
// and returns a proper error message with the probe reason when source_code search is unavailable
|
|
153
|
+
const schema = getToolSchema(toolName, isBtp);
|
|
154
|
+
if (schema) {
|
|
155
|
+
const parsed = schema.safeParse(args);
|
|
156
|
+
if (!parsed.success) {
|
|
157
|
+
const validationError = formatZodError(parsed.error, toolName);
|
|
158
|
+
logger.emitAudit({
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
level: 'warn',
|
|
161
|
+
event: 'safety_blocked',
|
|
162
|
+
requestId: reqId,
|
|
163
|
+
user,
|
|
164
|
+
clientId,
|
|
165
|
+
operation: toolName,
|
|
166
|
+
reason: 'Input validation failed',
|
|
167
|
+
});
|
|
168
|
+
return errorResult(validationError);
|
|
169
|
+
}
|
|
170
|
+
args = parsed.data;
|
|
171
|
+
}
|
|
130
172
|
// Run within request context so HTTP-level logs get the requestId
|
|
131
173
|
return requestContext.run({ requestId: reqId, user, tool: toolName }, async () => {
|
|
132
174
|
try {
|
|
@@ -163,7 +205,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
163
205
|
result = await handleSAPContext(client, args, cachingLayer);
|
|
164
206
|
break;
|
|
165
207
|
case 'SAPManage':
|
|
166
|
-
result = await handleSAPManage(client, config, args, cachingLayer);
|
|
208
|
+
result = await handleSAPManage(client, config, args, cachingLayer, isPerUserClient);
|
|
167
209
|
break;
|
|
168
210
|
case 'SAP': {
|
|
169
211
|
// Hyperfocused mode: route to the appropriate handler
|
|
@@ -175,13 +217,13 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
175
217
|
// Check scope for the delegated action
|
|
176
218
|
if (authInfo) {
|
|
177
219
|
const requiredScope = getHyperfocusedScope(String(args.action ?? ''));
|
|
178
|
-
if (!authInfo
|
|
220
|
+
if (!hasRequiredScope(authInfo, requiredScope)) {
|
|
179
221
|
result = errorResult(`Insufficient scope: '${requiredScope}' required for SAP(action="${args.action}"). Your scopes: [${authInfo.scopes.join(', ')}]`);
|
|
180
222
|
break;
|
|
181
223
|
}
|
|
182
224
|
}
|
|
183
225
|
// Delegate to the real handler (recursive call, but with the mapped tool name)
|
|
184
|
-
result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer);
|
|
226
|
+
result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer, isPerUserClient);
|
|
185
227
|
break;
|
|
186
228
|
}
|
|
187
229
|
default:
|
|
@@ -257,10 +299,19 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
257
299
|
const { source } = await cachingLayer.getSource(objType, objName, fetcher);
|
|
258
300
|
return source;
|
|
259
301
|
};
|
|
302
|
+
// Structured format is only supported for CLAS type
|
|
303
|
+
if (args.format === 'structured' && type !== 'CLAS') {
|
|
304
|
+
return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
|
|
305
|
+
}
|
|
260
306
|
switch (type) {
|
|
261
307
|
case 'PROG':
|
|
262
308
|
return textResult(await cachedGet('PROG', name, () => client.getProgram(name)));
|
|
263
309
|
case 'CLAS': {
|
|
310
|
+
// Structured format: return JSON with metadata + decomposed source
|
|
311
|
+
if (args.format === 'structured') {
|
|
312
|
+
const structured = await client.getClassStructured(name);
|
|
313
|
+
return textResult(JSON.stringify(structured, null, 2));
|
|
314
|
+
}
|
|
264
315
|
const methodParam = args.method;
|
|
265
316
|
if (methodParam && !args.include) {
|
|
266
317
|
// Method-level read — fetch full source then extract
|
|
@@ -336,8 +387,17 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
336
387
|
return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
|
|
337
388
|
case 'SRVD':
|
|
338
389
|
return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
|
|
339
|
-
case 'DDLX':
|
|
340
|
-
|
|
390
|
+
case 'DDLX': {
|
|
391
|
+
try {
|
|
392
|
+
return textResult(await cachedGet('DDLX', name, () => client.getDdlx(name)));
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
if (isNotFoundError(err)) {
|
|
396
|
+
return textResult(`No metadata extension (DDLX) found for "${name}". This means no @UI annotations are defined via DDLX for this view. The view may use inline annotations in the DDLS source, or the Fiori app may configure columns via manifest.json / app descriptor.`);
|
|
397
|
+
}
|
|
398
|
+
throw err;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
341
401
|
case 'SRVB':
|
|
342
402
|
return textResult(await cachedGet('SRVB', name, () => client.getSrvb(name)));
|
|
343
403
|
case 'TABL':
|
|
@@ -421,7 +481,9 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
421
481
|
case 'VARIANTS':
|
|
422
482
|
return textResult(await client.getVariants(name));
|
|
423
483
|
default:
|
|
424
|
-
return errorResult(`Unknown SAPRead type: ${type}. Supported: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS`
|
|
484
|
+
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS. ` +
|
|
485
|
+
'Tip: Map objectType from SAPSearch results by dropping the slash suffix (e.g., DDLS/DF → type="DDLS", CLAS/OC → type="CLAS", PROG/P → type="PROG"). ' +
|
|
486
|
+
'Do not pass a URI — use the "type" and "name" parameters instead.');
|
|
425
487
|
}
|
|
426
488
|
}
|
|
427
489
|
async function handleSAPSearch(client, args) {
|
|
@@ -429,6 +491,11 @@ async function handleSAPSearch(client, args) {
|
|
|
429
491
|
const maxResults = Number(args.maxResults ?? 100);
|
|
430
492
|
const searchType = String(args.searchType ?? 'object');
|
|
431
493
|
if (searchType === 'source_code') {
|
|
494
|
+
// If probe already determined textSearch is unavailable, return the precise reason
|
|
495
|
+
if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
|
|
496
|
+
return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
|
|
497
|
+
`\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
|
|
498
|
+
}
|
|
432
499
|
const objectType = args.objectType;
|
|
433
500
|
const packageName = args.packageName;
|
|
434
501
|
try {
|
|
@@ -436,9 +503,13 @@ async function handleSAPSearch(client, args) {
|
|
|
436
503
|
return textResult(JSON.stringify(results, null, 2));
|
|
437
504
|
}
|
|
438
505
|
catch (err) {
|
|
439
|
-
if (err instanceof AdtApiError
|
|
440
|
-
|
|
441
|
-
|
|
506
|
+
if (err instanceof AdtApiError) {
|
|
507
|
+
const permanentCodes = [401, 403, 404, 501];
|
|
508
|
+
if (permanentCodes.includes(err.statusCode)) {
|
|
509
|
+
const classified = classifyTextSearchError(err.statusCode);
|
|
510
|
+
return errorResult(`Source code search is not available on this SAP system. ${classified.reason ?? ''}` +
|
|
511
|
+
`\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
|
|
512
|
+
}
|
|
442
513
|
}
|
|
443
514
|
throw err;
|
|
444
515
|
}
|
|
@@ -474,6 +545,10 @@ async function handleSAPQuery(client, args) {
|
|
|
474
545
|
}
|
|
475
546
|
}
|
|
476
547
|
}
|
|
548
|
+
// JOIN-aware error: ADT freestyle SQL parser has known edge cases with JOINs (SAP Note 3605050)
|
|
549
|
+
if (err instanceof AdtApiError && err.statusCode === 400 && /\bJOIN\b/i.test(sql)) {
|
|
550
|
+
return errorResult(`${err.message}\n\nMulti-table JOIN query failed. The ADT freestyle SQL endpoint has known parser edge cases with JOINs (SAP Note 3605050). Try splitting into separate single-table queries.`);
|
|
551
|
+
}
|
|
477
552
|
throw err;
|
|
478
553
|
}
|
|
479
554
|
}
|
|
@@ -540,6 +615,129 @@ function buildLintConfigOptions(config, ruleOverrides) {
|
|
|
540
615
|
ruleOverrides,
|
|
541
616
|
};
|
|
542
617
|
}
|
|
618
|
+
// ─── Object Creation XML ─────────────────────────────────────────────
|
|
619
|
+
/**
|
|
620
|
+
* Build the type-specific XML body for ADT object creation.
|
|
621
|
+
*
|
|
622
|
+
* SAP ADT requires each object type to have its own root XML element.
|
|
623
|
+
* Using a generic body (e.g. adtcore:objectReferences) returns 400:
|
|
624
|
+
* "System expected the element '{http://www.sap.com/adt/programs/programs}abapProgram'"
|
|
625
|
+
*/
|
|
626
|
+
export function buildCreateXml(type, name, pkg, description) {
|
|
627
|
+
switch (type) {
|
|
628
|
+
case 'PROG':
|
|
629
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
630
|
+
<program:abapProgram xmlns:program="http://www.sap.com/adt/programs/programs"
|
|
631
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
632
|
+
adtcore:description="${escapeXml(description)}"
|
|
633
|
+
adtcore:name="${escapeXml(name)}"
|
|
634
|
+
adtcore:type="PROG/P"
|
|
635
|
+
adtcore:masterLanguage="EN"
|
|
636
|
+
adtcore:masterSystem="H00"
|
|
637
|
+
adtcore:responsible="DEVELOPER">
|
|
638
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
639
|
+
</program:abapProgram>`;
|
|
640
|
+
case 'CLAS':
|
|
641
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
642
|
+
<class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes"
|
|
643
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
644
|
+
adtcore:description="${escapeXml(description)}"
|
|
645
|
+
adtcore:name="${escapeXml(name)}"
|
|
646
|
+
adtcore:type="CLAS/OC"
|
|
647
|
+
adtcore:masterLanguage="EN"
|
|
648
|
+
adtcore:masterSystem="H00"
|
|
649
|
+
adtcore:responsible="DEVELOPER">
|
|
650
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
651
|
+
</class:abapClass>`;
|
|
652
|
+
case 'INTF':
|
|
653
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
654
|
+
<intf:abapInterface xmlns:intf="http://www.sap.com/adt/oo/interfaces"
|
|
655
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
656
|
+
adtcore:description="${escapeXml(description)}"
|
|
657
|
+
adtcore:name="${escapeXml(name)}"
|
|
658
|
+
adtcore:type="INTF/OI"
|
|
659
|
+
adtcore:masterLanguage="EN"
|
|
660
|
+
adtcore:masterSystem="H00"
|
|
661
|
+
adtcore:responsible="DEVELOPER">
|
|
662
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
663
|
+
</intf:abapInterface>`;
|
|
664
|
+
case 'INCL':
|
|
665
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
666
|
+
<include:abapInclude xmlns:include="http://www.sap.com/adt/programs/includes"
|
|
667
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
668
|
+
adtcore:description="${escapeXml(description)}"
|
|
669
|
+
adtcore:name="${escapeXml(name)}"
|
|
670
|
+
adtcore:type="PROG/I"
|
|
671
|
+
adtcore:masterLanguage="EN"
|
|
672
|
+
adtcore:masterSystem="H00"
|
|
673
|
+
adtcore:responsible="DEVELOPER">
|
|
674
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
675
|
+
</include:abapInclude>`;
|
|
676
|
+
case 'DDLS':
|
|
677
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
678
|
+
<ddl:ddlSource xmlns:ddl="http://www.sap.com/adt/ddic/ddlsources"
|
|
679
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
680
|
+
adtcore:description="${escapeXml(description)}"
|
|
681
|
+
adtcore:name="${escapeXml(name)}"
|
|
682
|
+
adtcore:type="DDLS/DF"
|
|
683
|
+
adtcore:masterLanguage="EN"
|
|
684
|
+
adtcore:masterSystem="H00"
|
|
685
|
+
adtcore:responsible="DEVELOPER">
|
|
686
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
687
|
+
</ddl:ddlSource>`;
|
|
688
|
+
case 'BDEF':
|
|
689
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
690
|
+
<bdef:behaviorDefinition xmlns:bdef="http://www.sap.com/adt/bo/behaviordefinitions"
|
|
691
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
692
|
+
adtcore:description="${escapeXml(description)}"
|
|
693
|
+
adtcore:name="${escapeXml(name)}"
|
|
694
|
+
adtcore:type="BDEF/BDO"
|
|
695
|
+
adtcore:masterLanguage="EN"
|
|
696
|
+
adtcore:masterSystem="H00"
|
|
697
|
+
adtcore:responsible="DEVELOPER">
|
|
698
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
699
|
+
</bdef:behaviorDefinition>`;
|
|
700
|
+
case 'SRVD':
|
|
701
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
702
|
+
<srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvd/sources"
|
|
703
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
704
|
+
adtcore:description="${escapeXml(description)}"
|
|
705
|
+
adtcore:name="${escapeXml(name)}"
|
|
706
|
+
adtcore:type="SRVD/SRV"
|
|
707
|
+
adtcore:masterLanguage="EN"
|
|
708
|
+
adtcore:masterSystem="H00"
|
|
709
|
+
adtcore:responsible="DEVELOPER">
|
|
710
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
711
|
+
</srvd:srvdSource>`;
|
|
712
|
+
case 'DDLX':
|
|
713
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
714
|
+
<ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlx/sources"
|
|
715
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
716
|
+
adtcore:description="${escapeXml(description)}"
|
|
717
|
+
adtcore:name="${escapeXml(name)}"
|
|
718
|
+
adtcore:type="DDLX/EX"
|
|
719
|
+
adtcore:masterLanguage="EN"
|
|
720
|
+
adtcore:masterSystem="H00"
|
|
721
|
+
adtcore:responsible="DEVELOPER">
|
|
722
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
723
|
+
</ddlx:ddlxSource>`;
|
|
724
|
+
default:
|
|
725
|
+
// Fallback — generic objectReferences using the correct URL for the type
|
|
726
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
727
|
+
<adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">
|
|
728
|
+
<adtcore:objectReference adtcore:uri="${escapeXml(objectUrlForType(type, name))}" adtcore:type="${escapeXml(type)}" adtcore:name="${escapeXml(name)}" adtcore:packageName="${escapeXml(pkg)}"/>
|
|
729
|
+
</adtcore:objectReferences>`;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** Escape special characters for XML attribute values */
|
|
733
|
+
function escapeXml(s) {
|
|
734
|
+
return s
|
|
735
|
+
.replace(/&/g, '&')
|
|
736
|
+
.replace(/"/g, '"')
|
|
737
|
+
.replace(/'/g, ''')
|
|
738
|
+
.replace(/</g, '<')
|
|
739
|
+
.replace(/>/g, '>');
|
|
740
|
+
}
|
|
543
741
|
// ─── Object URL Mapping ──────────────────────────────────────────────
|
|
544
742
|
/** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. */
|
|
545
743
|
function objectUrlForType(type, name) {
|
|
@@ -592,6 +790,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
592
790
|
const name = String(args.name ?? '');
|
|
593
791
|
const source = String(args.source ?? '');
|
|
594
792
|
const transport = args.transport;
|
|
793
|
+
// type and name are required for all actions except batch_create
|
|
794
|
+
if (action !== 'batch_create' && (!type || !name)) {
|
|
795
|
+
return errorResult('"type" and "name" are required for this action.');
|
|
796
|
+
}
|
|
595
797
|
const objectUrl = objectUrlForType(type, name);
|
|
596
798
|
const srcUrl = sourceUrlForType(type, name);
|
|
597
799
|
switch (action) {
|
|
@@ -607,12 +809,33 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
607
809
|
}
|
|
608
810
|
case 'create': {
|
|
609
811
|
const pkg = String(args.package ?? '$TMP');
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
812
|
+
checkPackage(client.safety, pkg);
|
|
813
|
+
const description = String(args.description ?? name);
|
|
814
|
+
checkPackage(client.safety, pkg);
|
|
815
|
+
// AFF header validation (if schema available for this type)
|
|
816
|
+
const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
|
|
817
|
+
if (!affResult.valid) {
|
|
818
|
+
return errorResult(`AFF metadata validation failed for ${type} ${name}:\n- ${(affResult.errors ?? []).join('\n- ')}\n\nFix the metadata and retry.`);
|
|
819
|
+
}
|
|
820
|
+
// Build type-specific creation XML body.
|
|
821
|
+
// SAP ADT requires the root element to match the object type —
|
|
822
|
+
// a generic objectReferences body returns 400 "System expected the element ...".
|
|
823
|
+
const body = buildCreateXml(type, name, pkg, description);
|
|
824
|
+
// Step 1: Create the object (metadata only)
|
|
825
|
+
const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
|
|
826
|
+
const result = await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
|
|
827
|
+
// Step 2: Write source code if provided
|
|
828
|
+
if (source) {
|
|
829
|
+
// Pre-write lint validation
|
|
830
|
+
const lintWarnings = runPreWriteLint(source, type, name, config);
|
|
831
|
+
if (lintWarnings.blocked) {
|
|
832
|
+
return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
|
|
833
|
+
}
|
|
834
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
|
|
835
|
+
cachingLayer?.invalidate(type, name);
|
|
836
|
+
const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
|
|
837
|
+
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
838
|
+
}
|
|
616
839
|
return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
|
|
617
840
|
}
|
|
618
841
|
case 'edit_method': {
|
|
@@ -665,8 +888,104 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
665
888
|
cachingLayer?.invalidate(type, name);
|
|
666
889
|
return textResult(`Deleted ${type} ${name}.`);
|
|
667
890
|
}
|
|
891
|
+
case 'batch_create': {
|
|
892
|
+
const objects = args.objects;
|
|
893
|
+
if (!objects || !Array.isArray(objects) || objects.length === 0) {
|
|
894
|
+
return errorResult('"objects" array is required and must be non-empty for batch_create action.');
|
|
895
|
+
}
|
|
896
|
+
const pkg = String(args.package ?? '$TMP');
|
|
897
|
+
// Check package is allowed before starting any creates
|
|
898
|
+
checkPackage(client.safety, pkg);
|
|
899
|
+
const results = [];
|
|
900
|
+
for (const obj of objects) {
|
|
901
|
+
const objType = String(obj.type ?? '');
|
|
902
|
+
const objName = String(obj.name ?? '');
|
|
903
|
+
const objSource = obj.source ? String(obj.source) : undefined;
|
|
904
|
+
const objDescription = String(obj.description ?? objName);
|
|
905
|
+
// AFF header validation per object (if schema available)
|
|
906
|
+
const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
|
|
907
|
+
if (!affResult.valid) {
|
|
908
|
+
results.push({
|
|
909
|
+
type: objType,
|
|
910
|
+
name: objName,
|
|
911
|
+
status: 'failed',
|
|
912
|
+
error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
|
|
913
|
+
});
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
|
|
918
|
+
if (objSource) {
|
|
919
|
+
const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
|
|
920
|
+
if (lintWarnings.blocked) {
|
|
921
|
+
results.push({
|
|
922
|
+
type: objType,
|
|
923
|
+
name: objName,
|
|
924
|
+
status: 'failed',
|
|
925
|
+
error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
|
|
926
|
+
});
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Step 1: Create the object
|
|
931
|
+
const objUrl = objectUrlForType(objType, objName);
|
|
932
|
+
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
933
|
+
const body = buildCreateXml(objType, objName, pkg, objDescription);
|
|
934
|
+
await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
|
|
935
|
+
// Step 2: Write source if provided
|
|
936
|
+
if (objSource) {
|
|
937
|
+
const srcUrl = sourceUrlForType(objType, objName);
|
|
938
|
+
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, transport);
|
|
939
|
+
}
|
|
940
|
+
// Step 3: Activate the object
|
|
941
|
+
const activationResult = await activate(client.http, client.safety, objUrl);
|
|
942
|
+
if (!activationResult.success) {
|
|
943
|
+
results.push({
|
|
944
|
+
type: objType,
|
|
945
|
+
name: objName,
|
|
946
|
+
status: 'failed',
|
|
947
|
+
error: `activation failed: ${activationResult.messages.join('; ')}`,
|
|
948
|
+
});
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
cachingLayer?.invalidate(objType, objName);
|
|
952
|
+
results.push({ type: objType, name: objName, status: 'success' });
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
results.push({
|
|
956
|
+
type: objType,
|
|
957
|
+
name: objName,
|
|
958
|
+
status: 'failed',
|
|
959
|
+
error: err instanceof Error ? err.message : String(err),
|
|
960
|
+
});
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Add 'skipped' entries for objects that were never attempted due to early break
|
|
965
|
+
for (let i = results.length; i < objects.length; i++) {
|
|
966
|
+
const skipped = objects[i];
|
|
967
|
+
results.push({
|
|
968
|
+
type: String(skipped.type ?? ''),
|
|
969
|
+
name: String(skipped.name ?? ''),
|
|
970
|
+
status: 'failed',
|
|
971
|
+
error: 'skipped — stopped after previous failure',
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
const summary = results
|
|
975
|
+
.map((r) => `${r.name} (${r.type}) ${r.status === 'success' ? '✓' : `✗ — ${r.error}`}`)
|
|
976
|
+
.join(', ');
|
|
977
|
+
const successCount = results.filter((r) => r.status === 'success').length;
|
|
978
|
+
const hasFailure = results.some((r) => r.status === 'failed');
|
|
979
|
+
if (hasFailure) {
|
|
980
|
+
const cleanupHint = successCount > 0
|
|
981
|
+
? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
|
|
982
|
+
: '';
|
|
983
|
+
return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}`);
|
|
984
|
+
}
|
|
985
|
+
return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}`);
|
|
986
|
+
}
|
|
668
987
|
default:
|
|
669
|
-
return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method`);
|
|
988
|
+
return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create`);
|
|
670
989
|
}
|
|
671
990
|
}
|
|
672
991
|
/**
|
|
@@ -1033,7 +1352,7 @@ async function handleSAPContext(client, args, cachingLayer) {
|
|
|
1033
1352
|
// ─── SAPManage Handler ────────────────────────────────────────────────
|
|
1034
1353
|
/** Cached feature status — populated on first probe */
|
|
1035
1354
|
let cachedFeatures;
|
|
1036
|
-
async function handleSAPManage(client, config, args, cachingLayer) {
|
|
1355
|
+
async function handleSAPManage(client, config, args, cachingLayer, isPerUserClient) {
|
|
1037
1356
|
const action = String(args.action ?? '');
|
|
1038
1357
|
switch (action) {
|
|
1039
1358
|
case 'features': {
|
|
@@ -1063,11 +1382,30 @@ async function handleSAPManage(client, config, args, cachingLayer) {
|
|
|
1063
1382
|
featureConfig.amdp = config.featureAmdp;
|
|
1064
1383
|
featureConfig.ui5 = config.featureUi5;
|
|
1065
1384
|
featureConfig.transport = config.featureTransport;
|
|
1066
|
-
|
|
1067
|
-
|
|
1385
|
+
const probed = await probeFeatures(client.http, featureConfig, config.systemType);
|
|
1386
|
+
// In PP mode with a per-user client, auth-sensitive results (401/403 on any
|
|
1387
|
+
// feature) must not poison the global cache — another user may have different
|
|
1388
|
+
// authorizations. Return the per-user result to the caller but keep the global
|
|
1389
|
+
// cache unchanged. However, when PP is enabled but the request fell back to the
|
|
1390
|
+
// shared/default client (no JWT, missing btpConfig, or non-strict fallback), the
|
|
1391
|
+
// probe ran with the same service-account credentials as the startup probe, so
|
|
1392
|
+
// updating the cache is safe and allows a manual probe to repair a failed startup.
|
|
1393
|
+
// Apply the same auth-failure sanitization as the startup probe: in PP mode,
|
|
1394
|
+
// shared-client 401/403 on textSearch must not hide source_code from users who
|
|
1395
|
+
// might have authorization via per-user clients.
|
|
1396
|
+
if (!isPerUserClient) {
|
|
1397
|
+
if (config.ppEnabled && probed.textSearch && !probed.textSearch.available) {
|
|
1398
|
+
const reason = probed.textSearch.reason ?? '';
|
|
1399
|
+
if (reason.includes('authorization') || reason.includes('401') || reason.includes('403')) {
|
|
1400
|
+
probed.textSearch = undefined;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
cachedFeatures = probed;
|
|
1404
|
+
}
|
|
1405
|
+
return textResult(JSON.stringify(probed, null, 2));
|
|
1068
1406
|
}
|
|
1069
1407
|
default:
|
|
1070
|
-
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe`);
|
|
1408
|
+
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats`);
|
|
1071
1409
|
}
|
|
1072
1410
|
}
|
|
1073
1411
|
/** Reset cached features (for testing) */
|
|
@@ -1078,4 +1416,8 @@ export function resetCachedFeatures() {
|
|
|
1078
1416
|
export function setCachedFeatures(features) {
|
|
1079
1417
|
cachedFeatures = features;
|
|
1080
1418
|
}
|
|
1419
|
+
/** Get cached features (for tool definition adaptation) */
|
|
1420
|
+
export function getCachedFeatures() {
|
|
1421
|
+
return cachedFeatures;
|
|
1422
|
+
}
|
|
1081
1423
|
//# sourceMappingURL=intent.js.map
|