arc-1 0.9.11 → 0.9.12
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 +23 -30
- package/dist/adt/client.d.ts +1 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +5 -2
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/ddic-xml.d.ts +31 -0
- package/dist/adt/ddic-xml.d.ts.map +1 -1
- package/dist/adt/ddic-xml.js +33 -7
- package/dist/adt/ddic-xml.js.map +1 -1
- package/dist/adt/devtools.d.ts +22 -1
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +74 -0
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/server-driven.d.ts +84 -0
- package/dist/adt/server-driven.d.ts.map +1 -0
- package/dist/adt/server-driven.js +207 -0
- package/dist/adt/server-driven.js.map +1 -0
- package/dist/adt/types.d.ts +48 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +8 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +33 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +1 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/extract-sap-cookies.d.ts +12 -1
- package/dist/extract-sap-cookies.d.ts.map +1 -1
- package/dist/extract-sap-cookies.js +3 -3
- package/dist/extract-sap-cookies.js.map +1 -1
- package/dist/handlers/intent.d.ts +25 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +318 -64
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +82 -44
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +96 -56
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +126 -19
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/zod-errors.d.ts +2 -1
- package/dist/handlers/zod-errors.d.ts.map +1 -1
- package/dist/handlers/zod-errors.js +4 -2
- package/dist/handlers/zod-errors.js.map +1 -1
- package/dist/probe/catalog.d.ts.map +1 -1
- package/dist/probe/catalog.js +6 -0
- package/dist/probe/catalog.js.map +1 -1
- package/dist/server/audit.d.ts +18 -4
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/http.d.ts +25 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +67 -5
- package/dist/server/http.js.map +1 -1
- package/dist/server/oauth-state.d.ts +2 -0
- package/dist/server/oauth-state.d.ts.map +1 -1
- package/dist/server/oauth-state.js +5 -2
- package/dist/server/oauth-state.js.map +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +2 -2
- package/dist/server/server.js.map +1 -1
- package/dist/server/stateless-client-store.d.ts +71 -4
- package/dist/server/stateless-client-store.d.ts.map +1 -1
- package/dist/server/stateless-client-store.js +133 -4
- package/dist/server/stateless-client-store.js.map +1 -1
- package/dist/server/xsuaa.d.ts +83 -0
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +2 -1
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +7 -4
package/dist/handlers/intent.js
CHANGED
|
@@ -14,8 +14,8 @@ import { buildSiblingExtensionFinding, classifyCdsImpact, deriveSiblingStem, isS
|
|
|
14
14
|
import { diffMethodSets, extractMethodNameFromClause, findSectionAnchor, insertMethodPair, moveMethodDefinition, removeMethodPair, spliceClassDefinition, spliceMethodSignature, } from '../adt/class-structure.js';
|
|
15
15
|
import { findDefinition, findInterfaceImplementersViaSeoMetaRel, findReferences, findWhereUsed, getCompletion, getWhereUsedScope, } from '../adt/codeintel.js';
|
|
16
16
|
import { createObject, deleteObject, lockObject, safeUpdateClassInclude, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, updateSource, } from '../adt/crud.js';
|
|
17
|
-
import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, decodeKtdText, normalizeAdtLanguage, rewriteKtdText, } from '../adt/ddic-xml.js';
|
|
18
|
-
import { activate, activateBatch, applyFixProposal, getFixProposals, getPrettyPrinterSettings, prettyPrint, publishServiceBinding, runAtcCheck, runUnitTests, setPrettyPrinterSettings, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
17
|
+
import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, decodeKtdText, normalizeAdtLanguage, normalizeAdtResponsible, rewriteKtdText, } from '../adt/ddic-xml.js';
|
|
18
|
+
import { activate, activateBatch, applyFixProposal, getCdsTestCases, getFixProposals, getPrettyPrinterSettings, prettyPrint, publishServiceBinding, runAtcCheck, runUnitTests, setPrettyPrinterSettings, supportsCdsTestCases, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
19
19
|
import { getDump, getGatewayErrorDetail, getObjectState, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listGatewayErrors, listSystemMessages, listTraces, } from '../adt/diagnostics.js';
|
|
20
20
|
import { AdtApiError, AdtNetworkError, AdtSafetyError, classifySapDomainError, isNotFoundError, } from '../adt/errors.js';
|
|
21
21
|
import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
@@ -27,6 +27,7 @@ import { applyRapHandlerScaffold, extractRapHandlerRequirements, findMissingRapH
|
|
|
27
27
|
import { formatRapPreflightFindings, validateRapSource } from '../adt/rap-preflight.js';
|
|
28
28
|
import { changePackage } from '../adt/refactoring.js';
|
|
29
29
|
import { checkOperation, checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
30
|
+
import { createServerDrivenObject, deleteServerDrivenObject, getServerDrivenObject, isServerDrivenObjectType, serverDrivenBlueContentType, serverDrivenObjectUrl, supportsServerDrivenObject, updateServerDrivenObjectSource, } from '../adt/server-driven.js';
|
|
30
31
|
import { createTransport, createTransportWithTarget, deleteTransport, getObjectTransports, getTransport, getTransportInfo, listTransportLayers, listTransports, listTransportTargets, reassignTransport, releaseTransport, releaseTransportRecursive, supportsExplicitTransportTarget, } from '../adt/transport.js';
|
|
31
32
|
import { getAppInfo } from '../adt/ui5-repository.js';
|
|
32
33
|
import { validateAffHeader } from '../aff/validator.js';
|
|
@@ -825,19 +826,28 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
825
826
|
// For SAPSearch.tadir_lookup with source='db'|'both', synthesize a sub-action key so the
|
|
826
827
|
// sql-scoped policy entry kicks in (otherwise viewer-only profiles could piggyback on the
|
|
827
828
|
// ADT info-system route to issue freestyle SQL).
|
|
829
|
+
//
|
|
830
|
+
// SECURITY (privilege-escalation hardening): the scope key is derived from the SAME
|
|
831
|
+
// normalized value the handler ultimately dispatches on. `normalizeTypeArgsForValidation`
|
|
832
|
+
// upper-cases + slash-collapses `type` and coerces non-string inputs via String(), so a
|
|
833
|
+
// caller cannot evade the per-type scope gate by sending a value that misses the policy key
|
|
834
|
+
// here yet is canonicalized into a privileged type just before Zod runs. Two such bypasses
|
|
835
|
+
// existed when this lookup read the RAW `args`: an array (`type: ["TABLE_CONTENTS"]` —
|
|
836
|
+
// typeof "object" → undefined key → base `read`) and a lowercase string
|
|
837
|
+
// (`type: "table_contents"` — no `SAPRead.table_contents` key → base `read`), both of which
|
|
838
|
+
// were then normalized into the data-scoped `TABLE_CONTENTS` for the handler. Normalizing
|
|
839
|
+
// first closes the array, case, and slash-form variants in one place (and keeps the
|
|
840
|
+
// SAP_DENY_ACTIONS match below consistent with the canonical form). The normalized object is
|
|
841
|
+
// reused for Zod validation below so canonicalization happens exactly once.
|
|
828
842
|
// Runs BEFORE Zod validation so scope errors don't leak schema details to unauthorized callers.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
: undefined
|
|
833
|
-
: typeof args.action === 'string'
|
|
834
|
-
? args.action
|
|
835
|
-
: undefined;
|
|
843
|
+
const normalizedArgs = normalizeTypeArgsForValidation(toolName, args);
|
|
844
|
+
const rawScopeKey = toolName === 'SAPRead' ? normalizedArgs.type : normalizedArgs.action;
|
|
845
|
+
let actionOrType = rawScopeKey === undefined || rawScopeKey === null || rawScopeKey === '' ? undefined : String(rawScopeKey);
|
|
836
846
|
if (toolName === 'SAPSearch' &&
|
|
837
|
-
typeof
|
|
838
|
-
|
|
839
|
-
typeof
|
|
840
|
-
const src =
|
|
847
|
+
typeof normalizedArgs.searchType === 'string' &&
|
|
848
|
+
normalizedArgs.searchType === 'tadir_lookup' &&
|
|
849
|
+
typeof normalizedArgs.source === 'string') {
|
|
850
|
+
const src = normalizedArgs.source.toLowerCase();
|
|
841
851
|
if (src === 'db' || src === 'both') {
|
|
842
852
|
actionOrType = `tadir_lookup_${src}`;
|
|
843
853
|
}
|
|
@@ -881,7 +891,10 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
881
891
|
const isBtp = config.systemType === 'btp';
|
|
882
892
|
const schema = getToolSchema(toolName, isBtp);
|
|
883
893
|
if (schema) {
|
|
884
|
-
args
|
|
894
|
+
// Reuse the normalized args computed for the scope-key derivation above —
|
|
895
|
+
// re-normalizing would be redundant (the transform is idempotent) and risks
|
|
896
|
+
// the two paths drifting.
|
|
897
|
+
args = normalizedArgs;
|
|
885
898
|
const parsed = schema.safeParse(args);
|
|
886
899
|
if (!parsed.success) {
|
|
887
900
|
const validationError = formatZodError(parsed.error, toolName);
|
|
@@ -1098,6 +1111,20 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1098
1111
|
if (isBtpSystem() && BTP_HINTS[type]) {
|
|
1099
1112
|
return errorResult(BTP_HINTS[type]);
|
|
1100
1113
|
}
|
|
1114
|
+
// Server-driven objects (ABAP Platform 2025 / SAP_BASIS 8.16+): DESD, EVTB, DTSC, COTA, …
|
|
1115
|
+
// share one AFF generic-object contract (blue:blueSource metadata + AFF JSON source), read
|
|
1116
|
+
// via the discovery-gated generic engine instead of the per-type switch below. They bypass
|
|
1117
|
+
// the version/draft/cache machinery (no /source/main text; JSON output).
|
|
1118
|
+
if (isServerDrivenObjectType(type)) {
|
|
1119
|
+
if (!name)
|
|
1120
|
+
return errorResult(`"name" is required for SAPRead type=${type}.`);
|
|
1121
|
+
if (supportsServerDrivenObject(client.http, type) === false) {
|
|
1122
|
+
return errorResult(`SAPRead type=${type} (server-driven object) requires SAP_BASIS 8.16+ (ABAP Platform 2025 / S/4HANA 2025). ` +
|
|
1123
|
+
'This system does not expose this object type.');
|
|
1124
|
+
}
|
|
1125
|
+
const sdo = await getServerDrivenObject(client.http, client.safety, type, name);
|
|
1126
|
+
return textResult(JSON.stringify(sdo, null, 2));
|
|
1127
|
+
}
|
|
1101
1128
|
if (args.force_refresh === true && cachingLayer && VERSIONED_SOURCE_READ_TYPES.has(type)) {
|
|
1102
1129
|
cachingLayer.inactiveLists.invalidate(client.username);
|
|
1103
1130
|
cachingLayer.invalidate(type, name, 'all');
|
|
@@ -2322,12 +2349,18 @@ export function warnCdsReservedKeywords(source) {
|
|
|
2322
2349
|
return (`Warning: field name(s) ${fieldNames.map((f) => `'${f}'`).join(', ')} may be CDS reserved keywords. ` +
|
|
2323
2350
|
`If the DDL save fails with a generic syntax error, rename them (e.g., 'position' → 'playing_position', 'type' → 'obj_type').`);
|
|
2324
2351
|
}
|
|
2325
|
-
export function buildCreateXml(type, name, pkg, description, properties, language) {
|
|
2352
|
+
export function buildCreateXml(type, name, pkg, description, properties, language, responsible) {
|
|
2326
2353
|
// Master/original language for the created object. Derived from the configured
|
|
2327
2354
|
// SAP_LANGUAGE (passed by callers as config.language) so the create-XML body
|
|
2328
2355
|
// matches the sap-language URL param ARC-1 already sends. Defaults to "EN" when
|
|
2329
2356
|
// unset, preserving legacy output. See issue #343.
|
|
2330
2357
|
const masterLanguage = normalizeAdtLanguage(language);
|
|
2358
|
+
// Person responsible for the created object. Derived from the configured logon
|
|
2359
|
+
// user (passed by callers as config.username). The legacy hard-coded "DEVELOPER"
|
|
2360
|
+
// only exists on SAP demo systems, so on a real system it fails with
|
|
2361
|
+
// 400 [?/049] "Enter a valid user, not DEVELOPER, as the person responsible".
|
|
2362
|
+
// Defaults to "DEVELOPER" only when no user is configured. Same threading as #343.
|
|
2363
|
+
const responsibleUser = normalizeAdtResponsible(responsible);
|
|
2331
2364
|
switch (type) {
|
|
2332
2365
|
case 'PROG':
|
|
2333
2366
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -2338,7 +2371,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2338
2371
|
adtcore:type="PROG/P"
|
|
2339
2372
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2340
2373
|
adtcore:masterSystem="H00"
|
|
2341
|
-
adtcore:responsible="
|
|
2374
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2342
2375
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2343
2376
|
</program:abapProgram>`;
|
|
2344
2377
|
case 'CLAS':
|
|
@@ -2350,7 +2383,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2350
2383
|
adtcore:type="CLAS/OC"
|
|
2351
2384
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2352
2385
|
adtcore:masterSystem="H00"
|
|
2353
|
-
adtcore:responsible="
|
|
2386
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2354
2387
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2355
2388
|
</class:abapClass>`;
|
|
2356
2389
|
case 'INTF':
|
|
@@ -2362,7 +2395,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2362
2395
|
adtcore:type="INTF/OI"
|
|
2363
2396
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2364
2397
|
adtcore:masterSystem="H00"
|
|
2365
|
-
adtcore:responsible="
|
|
2398
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2366
2399
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2367
2400
|
</intf:abapInterface>`;
|
|
2368
2401
|
case 'INCL':
|
|
@@ -2374,7 +2407,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2374
2407
|
adtcore:type="PROG/I"
|
|
2375
2408
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2376
2409
|
adtcore:masterSystem="H00"
|
|
2377
|
-
adtcore:responsible="
|
|
2410
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2378
2411
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2379
2412
|
</include:abapInclude>`;
|
|
2380
2413
|
case 'DDLS':
|
|
@@ -2386,7 +2419,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2386
2419
|
adtcore:type="DDLS/DF"
|
|
2387
2420
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2388
2421
|
adtcore:masterSystem="H00"
|
|
2389
|
-
adtcore:responsible="
|
|
2422
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2390
2423
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2391
2424
|
</ddl:ddlSource>`;
|
|
2392
2425
|
case 'DCLS':
|
|
@@ -2398,7 +2431,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2398
2431
|
adtcore:type="DCLS/DL"
|
|
2399
2432
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2400
2433
|
adtcore:masterSystem="H00"
|
|
2401
|
-
adtcore:responsible="
|
|
2434
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2402
2435
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2403
2436
|
</dcl:dclSource>`;
|
|
2404
2437
|
case 'TABL':
|
|
@@ -2416,7 +2449,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2416
2449
|
adtcore:type="${adtType}"
|
|
2417
2450
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2418
2451
|
adtcore:masterSystem="H00"
|
|
2419
|
-
adtcore:responsible="
|
|
2452
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2420
2453
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2421
2454
|
</blue:blueSource>`;
|
|
2422
2455
|
}
|
|
@@ -2431,7 +2464,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2431
2464
|
adtcore:type="BDEF/BDO"
|
|
2432
2465
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2433
2466
|
adtcore:masterSystem="H00"
|
|
2434
|
-
adtcore:responsible="
|
|
2467
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2435
2468
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2436
2469
|
</blue:blueSource>`;
|
|
2437
2470
|
case 'SRVD':
|
|
@@ -2443,7 +2476,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2443
2476
|
adtcore:type="SRVD/SRV"
|
|
2444
2477
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2445
2478
|
adtcore:masterSystem="H00"
|
|
2446
|
-
adtcore:responsible="
|
|
2479
|
+
adtcore:responsible="${escapeXml(responsibleUser)}"
|
|
2447
2480
|
srvd:srvdSourceType="S">
|
|
2448
2481
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2449
2482
|
</srvd:srvdSource>`;
|
|
@@ -2464,6 +2497,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2464
2497
|
version: properties?.version ? String(properties.version) : undefined,
|
|
2465
2498
|
odataVersion: properties?.odataVersion ? String(properties.odataVersion) : undefined,
|
|
2466
2499
|
language: masterLanguage,
|
|
2500
|
+
responsible: responsibleUser,
|
|
2467
2501
|
};
|
|
2468
2502
|
return buildServiceBindingXml(params);
|
|
2469
2503
|
}
|
|
@@ -2476,7 +2510,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2476
2510
|
adtcore:type="DDLX/EX"
|
|
2477
2511
|
adtcore:masterLanguage="${masterLanguage}"
|
|
2478
2512
|
adtcore:masterSystem="H00"
|
|
2479
|
-
adtcore:responsible="
|
|
2513
|
+
adtcore:responsible="${escapeXml(responsibleUser)}">
|
|
2480
2514
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2481
2515
|
</ddlx:ddlxSource>`;
|
|
2482
2516
|
case 'DOMA': {
|
|
@@ -2502,6 +2536,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2502
2536
|
fixedValues,
|
|
2503
2537
|
valueTable: properties?.valueTable ? String(properties.valueTable) : undefined,
|
|
2504
2538
|
language: masterLanguage,
|
|
2539
|
+
responsible: responsibleUser,
|
|
2505
2540
|
};
|
|
2506
2541
|
return buildDomainXml(params);
|
|
2507
2542
|
}
|
|
@@ -2528,6 +2563,7 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
|
|
|
2528
2563
|
defaultComponentName: properties?.defaultComponentName ? String(properties.defaultComponentName) : undefined,
|
|
2529
2564
|
changeDocument: toBoolean(properties?.changeDocument),
|
|
2530
2565
|
language: masterLanguage,
|
|
2566
|
+
responsible: responsibleUser,
|
|
2531
2567
|
};
|
|
2532
2568
|
return buildDataElementXml(params);
|
|
2533
2569
|
}
|
|
@@ -2758,63 +2794,125 @@ function normalizeWriteObjectType(type) {
|
|
|
2758
2794
|
function canonicalTablType(type) {
|
|
2759
2795
|
return type === 'TABL/DT' || type === 'TABL/DS' ? 'TABL' : type;
|
|
2760
2796
|
}
|
|
2761
|
-
/**
|
|
2762
|
-
|
|
2797
|
+
/**
|
|
2798
|
+
* Fields whose handler INTENTIONALLY treats an explicit empty string as a meaningful
|
|
2799
|
+
* signal distinct from "omitted", so the pre-validation strip must keep an empty-STRING
|
|
2800
|
+
* value (a `null` is still stripped — the handlers treat null as omitted):
|
|
2801
|
+
* - `target` — SAPTransport create rejects a "provided but empty" target as a caller
|
|
2802
|
+
* mistake (vs omitted → local); see `targetProvided` in handleSAPTransport.
|
|
2803
|
+
* - `proposalUserContent` — SAPDiagnose apply_quickfix forwards an empty
|
|
2804
|
+
* `<userContent></userContent>` verbatim.
|
|
2805
|
+
* Keep this set minimal: it only needs entries where a handler distinguishes ""-present
|
|
2806
|
+
* from absent. Empty strings on enums/numbers/everything-else are safe to strip.
|
|
2807
|
+
*/
|
|
2808
|
+
const EMPTY_STRING_MEANINGFUL_FIELDS = new Set(['target', 'proposalUserContent']);
|
|
2809
|
+
/**
|
|
2810
|
+
* Strip GPT/OpenAI "overpopulation" pollution before Zod validation:
|
|
2811
|
+
* - `null` values — OpenAI Structured Outputs / `strict` mode (the default for the
|
|
2812
|
+
* Responses API) emulates an optional field as a `["type","null"]` union and emits
|
|
2813
|
+
* `null` for every unused optional. `z.X().optional()` rejects `null`, so a strict
|
|
2814
|
+
* caller otherwise cannot make a clean call (every unused optional becomes null →
|
|
2815
|
+
* rejected). `null` is ALWAYS stripped (handlers treat null as omitted).
|
|
2816
|
+
* - empty / whitespace-only strings — many callers serialize an omitted optional as
|
|
2817
|
+
* `""`. On optional enums that hard-rejects; on optional numbers `z.coerce.number("")`
|
|
2818
|
+
* silently becomes `0`. Stripped, EXCEPT for the EMPTY_STRING_MEANINGFUL_FIELDS above.
|
|
2819
|
+
*
|
|
2820
|
+
* Preserves real `false` and `0` — ONLY `null` and empty/whitespace strings are removed.
|
|
2821
|
+
* Shallow at the top level, plus one level into each `objects[]` item (SAPWrite
|
|
2822
|
+
* `batch_create` / SAPActivate batch). Deliberately does NOT recurse into leaf data
|
|
2823
|
+
* arrays (`messages`/`fixedValues`/`parameters`/`where`) — those carry user data where
|
|
2824
|
+
* an empty string or null may be meaningful. See issue #360.
|
|
2825
|
+
*/
|
|
2826
|
+
export function stripLlmEmptyValues(args) {
|
|
2827
|
+
const isStrippable = (key, v) => v === null || (typeof v === 'string' && v.trim() === '' && !EMPTY_STRING_MEANINGFUL_FIELDS.has(key));
|
|
2828
|
+
const cleanShallow = (obj) => {
|
|
2829
|
+
const out = {};
|
|
2830
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
2831
|
+
if (!isStrippable(k, v))
|
|
2832
|
+
out[k] = v;
|
|
2833
|
+
}
|
|
2834
|
+
return out;
|
|
2835
|
+
};
|
|
2836
|
+
const cleaned = cleanShallow(args);
|
|
2837
|
+
if (Array.isArray(cleaned.objects)) {
|
|
2838
|
+
cleaned.objects = cleaned.objects.map((o) => o && typeof o === 'object' && !Array.isArray(o) ? cleanShallow(o) : o);
|
|
2839
|
+
}
|
|
2840
|
+
return cleaned;
|
|
2841
|
+
}
|
|
2842
|
+
/** Normalize type fields before schema validation so slash/case aliases are accepted.
|
|
2843
|
+
* Also strips GPT/OpenAI pollution (null + empty strings) via stripLlmEmptyValues so the
|
|
2844
|
+
* same normalization runs for every tool — standard, hyperfocused, and the CLI all route
|
|
2845
|
+
* through handleToolCall, which calls this once before scope derivation + Zod (issue #360).
|
|
2846
|
+
* Exported for unit tests (the include-drop + strip behavior). */
|
|
2847
|
+
export function normalizeTypeArgsForValidation(toolName, args) {
|
|
2848
|
+
const cleaned = stripLlmEmptyValues(args);
|
|
2763
2849
|
switch (toolName) {
|
|
2764
2850
|
case 'SAPRead':
|
|
2765
2851
|
return {
|
|
2766
|
-
...
|
|
2767
|
-
type: normalizeObjectType(String(
|
|
2768
|
-
objectType:
|
|
2852
|
+
...cleaned,
|
|
2853
|
+
type: normalizeObjectType(String(cleaned.type ?? '')),
|
|
2854
|
+
objectType: cleaned.objectType === undefined ? undefined : normalizeObjectType(String(cleaned.objectType ?? '')),
|
|
2769
2855
|
};
|
|
2770
|
-
case 'SAPWrite':
|
|
2856
|
+
case 'SAPWrite': {
|
|
2771
2857
|
// SAPWrite preserves TABL/DT and TABL/DS so the create path can route by subtype.
|
|
2858
|
+
const normType = cleaned.type === undefined ? undefined : normalizeWriteObjectType(String(cleaned.type ?? ''));
|
|
2859
|
+
// Drop an inapplicable `include`: it is only meaningful for a CLAS local-include
|
|
2860
|
+
// write (update/edit_method/edit_class_definition). GPT/OpenAI callers frequently
|
|
2861
|
+
// attach include="definitions" to unrelated writes (DDLS/PROG/DTEL/delete/batch_create),
|
|
2862
|
+
// which validateSapWriteInput would otherwise hard-reject even though the requested
|
|
2863
|
+
// intent is valid. A garbage include VALUE on a real CLAS include path is still
|
|
2864
|
+
// rejected by the z.enum check downstream (issue #360).
|
|
2865
|
+
const action = String(cleaned.action ?? '');
|
|
2866
|
+
const includeApplies = normType === 'CLAS' && (action === 'update' || action === 'edit_method' || action === 'edit_class_definition');
|
|
2867
|
+
if (!includeApplies)
|
|
2868
|
+
delete cleaned.include;
|
|
2772
2869
|
return {
|
|
2773
|
-
...
|
|
2774
|
-
type:
|
|
2775
|
-
objects: Array.isArray(
|
|
2776
|
-
?
|
|
2870
|
+
...cleaned,
|
|
2871
|
+
type: normType,
|
|
2872
|
+
objects: Array.isArray(cleaned.objects)
|
|
2873
|
+
? cleaned.objects.map((obj) => typeof obj === 'object' && obj !== null
|
|
2777
2874
|
? {
|
|
2778
2875
|
...obj,
|
|
2779
2876
|
type: normalizeWriteObjectType(String(obj.type ?? '')),
|
|
2780
2877
|
}
|
|
2781
2878
|
: obj)
|
|
2782
|
-
:
|
|
2879
|
+
: cleaned.objects,
|
|
2783
2880
|
};
|
|
2881
|
+
}
|
|
2784
2882
|
case 'SAPActivate':
|
|
2785
2883
|
return {
|
|
2786
|
-
...
|
|
2787
|
-
type:
|
|
2788
|
-
objects: Array.isArray(
|
|
2789
|
-
?
|
|
2884
|
+
...cleaned,
|
|
2885
|
+
type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
|
|
2886
|
+
objects: Array.isArray(cleaned.objects)
|
|
2887
|
+
? cleaned.objects.map((obj) => typeof obj === 'object' && obj !== null
|
|
2790
2888
|
? {
|
|
2791
2889
|
...obj,
|
|
2792
2890
|
type: normalizeObjectType(String(obj.type ?? '')),
|
|
2793
2891
|
}
|
|
2794
2892
|
: obj)
|
|
2795
|
-
:
|
|
2893
|
+
: cleaned.objects,
|
|
2796
2894
|
};
|
|
2797
2895
|
case 'SAPSearch':
|
|
2798
2896
|
return {
|
|
2799
|
-
...
|
|
2800
|
-
objectType:
|
|
2897
|
+
...cleaned,
|
|
2898
|
+
objectType: cleaned.objectType === undefined ? undefined : normalizeObjectType(String(cleaned.objectType ?? '')),
|
|
2801
2899
|
};
|
|
2802
2900
|
case 'SAPNavigate':
|
|
2803
2901
|
// Only normalize `type` (for URL building). `objectType` is passed to SAP's
|
|
2804
2902
|
// where-used scope API in slash format (e.g., CLAS/OC) — normalizing it would break the filter.
|
|
2805
2903
|
return {
|
|
2806
|
-
...
|
|
2807
|
-
type:
|
|
2904
|
+
...cleaned,
|
|
2905
|
+
type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
|
|
2808
2906
|
};
|
|
2809
2907
|
case 'SAPDiagnose':
|
|
2810
2908
|
return {
|
|
2811
|
-
...
|
|
2812
|
-
type:
|
|
2909
|
+
...cleaned,
|
|
2910
|
+
type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
|
|
2813
2911
|
};
|
|
2814
2912
|
case 'SAPContext':
|
|
2815
2913
|
return {
|
|
2816
|
-
...
|
|
2817
|
-
type:
|
|
2914
|
+
...cleaned,
|
|
2915
|
+
type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
|
|
2818
2916
|
};
|
|
2819
2917
|
case 'SAPTransport':
|
|
2820
2918
|
// Normalize `type` for SAPTransport actions that route through
|
|
@@ -2824,11 +2922,11 @@ function normalizeTypeArgsForValidation(toolName, args) {
|
|
|
2824
2922
|
// string-typed schema and hit the slash-form throw inside objectBasePath,
|
|
2825
2923
|
// which is correct as a last-resort fence but not as a friendly error.
|
|
2826
2924
|
return {
|
|
2827
|
-
...
|
|
2828
|
-
type:
|
|
2925
|
+
...cleaned,
|
|
2926
|
+
type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
|
|
2829
2927
|
};
|
|
2830
2928
|
default:
|
|
2831
|
-
return
|
|
2929
|
+
return cleaned;
|
|
2832
2930
|
}
|
|
2833
2931
|
}
|
|
2834
2932
|
/**
|
|
@@ -3018,6 +3116,112 @@ function stripIncludeHeader(source) {
|
|
|
3018
3116
|
* the `objects[]` items and are validated per item in the batch_create branch.
|
|
3019
3117
|
*/
|
|
3020
3118
|
const NAME_CASE_GUARD_ACTIONS = new Set(['create', 'update', 'edit_method', 'delete']);
|
|
3119
|
+
/**
|
|
3120
|
+
* Enforce the `allowedPackages` ceiling for an EXISTING object addressed by its
|
|
3121
|
+
* ADT object URL. Resolves the object's REAL package from ADT metadata and gates
|
|
3122
|
+
* it via `checkPackage`. Fail-closed: if the package can't be determined, refuse
|
|
3123
|
+
* the operation rather than passing the gate. No-op (and no HTTP round-trip) when
|
|
3124
|
+
* no package restrictions are configured.
|
|
3125
|
+
*
|
|
3126
|
+
* Shared by every mutating operation that targets an existing object —
|
|
3127
|
+
* update/delete/surgery (via `enforcePackageForExistingObject`), activation, and
|
|
3128
|
+
* change_package — so they all honor the same package boundary against the
|
|
3129
|
+
* object's true package, never a caller-supplied package string.
|
|
3130
|
+
*/
|
|
3131
|
+
async function enforceAllowedPackageForObjectUrl(client, objectUrl, label, accept) {
|
|
3132
|
+
if (client.safety.allowedPackages.length === 0)
|
|
3133
|
+
return undefined;
|
|
3134
|
+
const pkg = await client.resolveObjectPackage(objectUrl, accept);
|
|
3135
|
+
if (!pkg) {
|
|
3136
|
+
throw new AdtSafetyError(`${label} blocked: ARC-1 could not determine the object's package from ADT metadata ` +
|
|
3137
|
+
`(no adtcore:packageRef/containerRef). Fail-closed because allowedPackages is restricted.`);
|
|
3138
|
+
}
|
|
3139
|
+
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
3140
|
+
return pkg;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* SAPWrite for server-driven objects (8.16+): create / update-source / delete via the generic AFF
|
|
3144
|
+
* blue:blueSource + JSON-source engine. Discovery-gated (clean 8.16 error otherwise), allowWrites-gated
|
|
3145
|
+
* (through the engine's checkOperation), and allowedPackages-gated against the REAL package
|
|
3146
|
+
* (create gates the caller-supplied package like every create; update/delete resolve the object's true
|
|
3147
|
+
* package under the blues Accept). The `source` param carries the AFF JSON — parse-validated before the
|
|
3148
|
+
* PUT; ABAP-specific pre-write steps (lint, RAP preflight, CDS guard) do not apply. Create leaves the
|
|
3149
|
+
* object inactive — callers follow with SAPActivate (never auto-activated).
|
|
3150
|
+
*/
|
|
3151
|
+
async function handleServerDrivenObjectWrite(client, action, type, name, args, cachingLayer) {
|
|
3152
|
+
// Discovery gate — mirror handleSAPRead's server-driven branch.
|
|
3153
|
+
if (supportsServerDrivenObject(client.http, type) === false) {
|
|
3154
|
+
return errorResult(`SAPWrite type=${type} (server-driven object) requires SAP_BASIS 8.16+ (ABAP Platform 2025 / S/4HANA 2025). ` +
|
|
3155
|
+
'This system does not expose this object type.');
|
|
3156
|
+
}
|
|
3157
|
+
const transport = args.transport;
|
|
3158
|
+
const objUrl = serverDrivenObjectUrl(type, name);
|
|
3159
|
+
const blueAccept = serverDrivenBlueContentType(type);
|
|
3160
|
+
const invalidate = () => {
|
|
3161
|
+
cachingLayer?.invalidate(type, name, 'all');
|
|
3162
|
+
cachingLayer?.inactiveLists.invalidate(client.username);
|
|
3163
|
+
};
|
|
3164
|
+
// SDO source is AFF JSON (not ABAP) — validate it parses before any PUT.
|
|
3165
|
+
const validateSource = () => {
|
|
3166
|
+
const src = String(args.source ?? '');
|
|
3167
|
+
try {
|
|
3168
|
+
JSON.parse(src);
|
|
3169
|
+
}
|
|
3170
|
+
catch {
|
|
3171
|
+
return {
|
|
3172
|
+
ok: false,
|
|
3173
|
+
result: errorResult(`SAPWrite ${action} for ${type} ${name}: "source" must be valid AFF JSON ` +
|
|
3174
|
+
'(e.g. {"formatVersion":"1","header":{"description":"…","originalLanguage":"en"}}).'),
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
return { ok: true, json: src };
|
|
3178
|
+
};
|
|
3179
|
+
const hasSourceArg = typeof args.source === 'string' && args.source.trim() !== '';
|
|
3180
|
+
switch (action) {
|
|
3181
|
+
case 'create': {
|
|
3182
|
+
const pkg = String(args.package ?? '$TMP');
|
|
3183
|
+
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
3184
|
+
const description = String(args.description ?? name);
|
|
3185
|
+
await createServerDrivenObject(client.http, client.safety, type, name, {
|
|
3186
|
+
package: pkg,
|
|
3187
|
+
description,
|
|
3188
|
+
transport,
|
|
3189
|
+
});
|
|
3190
|
+
let wroteSource = false;
|
|
3191
|
+
if (hasSourceArg) {
|
|
3192
|
+
const v = validateSource();
|
|
3193
|
+
if (!v.ok)
|
|
3194
|
+
return v.result;
|
|
3195
|
+
await updateServerDrivenObjectSource(client.http, client.safety, type, name, v.json, { transport });
|
|
3196
|
+
wroteSource = true;
|
|
3197
|
+
}
|
|
3198
|
+
invalidate();
|
|
3199
|
+
return textResult(`Created ${type} ${name} in package ${pkg}${wroteSource ? ' and wrote AFF JSON source' : ''}.\n` +
|
|
3200
|
+
`Next step: SAPActivate(type="${type}", name="${name}").`);
|
|
3201
|
+
}
|
|
3202
|
+
case 'update': {
|
|
3203
|
+
if (!hasSourceArg) {
|
|
3204
|
+
return errorResult(`SAPWrite update for ${type} ${name} requires "source" (the AFF JSON body).`);
|
|
3205
|
+
}
|
|
3206
|
+
const v = validateSource();
|
|
3207
|
+
if (!v.ok)
|
|
3208
|
+
return v.result;
|
|
3209
|
+
await enforceAllowedPackageForObjectUrl(client, objUrl, `Operations on ${type} '${name}'`, blueAccept);
|
|
3210
|
+
await updateServerDrivenObjectSource(client.http, client.safety, type, name, v.json, { transport });
|
|
3211
|
+
invalidate();
|
|
3212
|
+
return textResult(`Updated source of ${type} ${name}.\nNext step: SAPActivate(type="${type}", name="${name}").`);
|
|
3213
|
+
}
|
|
3214
|
+
case 'delete': {
|
|
3215
|
+
await enforceAllowedPackageForObjectUrl(client, objUrl, `Operations on ${type} '${name}'`, blueAccept);
|
|
3216
|
+
await deleteServerDrivenObject(client.http, client.safety, type, name, { transport });
|
|
3217
|
+
invalidate();
|
|
3218
|
+
return textResult(`Deleted ${type} ${name}.`);
|
|
3219
|
+
}
|
|
3220
|
+
default:
|
|
3221
|
+
return errorResult(`Action "${action}" is not supported for server-driven object type ${type}. ` +
|
|
3222
|
+
'Supported: create, update, delete (source is AFF JSON) — then SAPActivate to activate.');
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3021
3225
|
async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
3022
3226
|
const action = String(args.action ?? '');
|
|
3023
3227
|
const type = normalizeWriteObjectType(String(args.type ?? ''));
|
|
@@ -3052,6 +3256,14 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3052
3256
|
`Note: the object NAME in TADIR must be uppercase, but the source code inside the object can use mixed case ` +
|
|
3053
3257
|
`(e.g. for DDLS: name="${name.toUpperCase()}" but source can contain "define view entity ${name}").`);
|
|
3054
3258
|
}
|
|
3259
|
+
// Server-driven objects (ABAP Platform 2025 / SAP_BASIS 8.16+): DESD, EVTB, DTSC, CSNM, EVTO, COTA
|
|
3260
|
+
// share one AFF generic-object write contract (POST blue:blueSource metadata → PUT AFF JSON source
|
|
3261
|
+
// → activate). They route through the dedicated engine instead of the per-type switch below —
|
|
3262
|
+
// objectBasePath(<sdo>) throws, so this MUST come before the objectUrl computation. Mirrors the
|
|
3263
|
+
// server-driven branch in handleSAPRead.
|
|
3264
|
+
if (isServerDrivenObjectType(type)) {
|
|
3265
|
+
return handleServerDrivenObjectWrite(client, action, type, name, args, cachingLayer);
|
|
3266
|
+
}
|
|
3055
3267
|
// For TABL update/delete/edit_method, the existing object may live at /tables/
|
|
3056
3268
|
// (transparent) or /structures/ (DDIC structure). Resolve once via the client's
|
|
3057
3269
|
// cached URL probe. For 'create' the default /tables/ URL is correct (we only
|
|
@@ -3128,15 +3340,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3128
3340
|
// Fail-closed: if the package cannot be determined from ADT metadata, refuse the write
|
|
3129
3341
|
// rather than silently passing through the allowlist gate.
|
|
3130
3342
|
async function enforcePackageForExistingObject() {
|
|
3131
|
-
|
|
3132
|
-
return undefined;
|
|
3133
|
-
const pkg = await client.resolveObjectPackage(objectUrl);
|
|
3134
|
-
if (!pkg) {
|
|
3135
|
-
throw new AdtSafetyError(`Operations on ${type} '${name}' blocked: ARC-1 could not determine the object's package ` +
|
|
3136
|
-
`from ADT metadata (no adtcore:packageRef in response). Fail-closed because allowedPackages is restricted.`);
|
|
3137
|
-
}
|
|
3138
|
-
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
3139
|
-
return pkg;
|
|
3343
|
+
return enforceAllowedPackageForObjectUrl(client, objectUrl, `Operations on ${type} '${name}'`);
|
|
3140
3344
|
}
|
|
3141
3345
|
// Helper for class-section surgery (issue #303): fetch the class structure AND
|
|
3142
3346
|
// /source/main at the SAME effective version, so the spliced line ranges line
|
|
@@ -3200,7 +3404,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3200
3404
|
const mergedProps = await mergeMetadataWriteProperties(client, type, name, metadataProps);
|
|
3201
3405
|
const description = String(args.description ?? mergedProps._description ?? name);
|
|
3202
3406
|
const pkg = String(args.package ?? existingPackage ?? mergedProps._package ?? '$TMP');
|
|
3203
|
-
const body = buildCreateXml(type, name, pkg, description, mergedProps, config.language);
|
|
3407
|
+
const body = buildCreateXml(type, name, pkg, description, mergedProps, config.language, config.username);
|
|
3204
3408
|
await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport, cachedFeatures?.abapRelease);
|
|
3205
3409
|
invalidateWrittenObject(type, name);
|
|
3206
3410
|
return textResult(`Successfully updated ${type} ${name}.`);
|
|
@@ -3385,7 +3589,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3385
3589
|
// SAP ADT requires the root element to match the object type —
|
|
3386
3590
|
// a generic objectReferences body returns 400 "System expected the element ...".
|
|
3387
3591
|
const metadataProperties = getMetadataWriteProperties(args);
|
|
3388
|
-
const body = buildCreateXml(type, name, pkg, description, metadataProperties, config.language);
|
|
3592
|
+
const body = buildCreateXml(type, name, pkg, description, metadataProperties, config.language, config.username);
|
|
3389
3593
|
// Step 1: Create the object (metadata only)
|
|
3390
3594
|
const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
|
|
3391
3595
|
// DOMA/DTEL/BDEF require vendor-specific content types; all other types use
|
|
@@ -4283,7 +4487,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
4283
4487
|
const objUrl = objectUrlForType(objType, objName);
|
|
4284
4488
|
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
4285
4489
|
const objMetadataProps = getMetadataWriteProperties(obj);
|
|
4286
|
-
const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps, config.language);
|
|
4490
|
+
const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps, config.language, config.username);
|
|
4287
4491
|
const contentType = createContentTypeForType(objType);
|
|
4288
4492
|
const needsPackageParam = objType === 'BDEF' || objType === 'TABL' || objType === 'TABL/DT' || objType === 'TABL/DS';
|
|
4289
4493
|
try {
|
|
@@ -4753,6 +4957,12 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4753
4957
|
}
|
|
4754
4958
|
return { type: objType, name: objName, url };
|
|
4755
4959
|
}));
|
|
4960
|
+
// Enforce the allowedPackages ceiling against each object's REAL package before
|
|
4961
|
+
// activating ANY of them — one out-of-allowlist object aborts the whole batch
|
|
4962
|
+
// (no partial activation). Fail-closed; no-op when unrestricted. (security audit 2026-06)
|
|
4963
|
+
for (const o of objects) {
|
|
4964
|
+
await enforceAllowedPackageForObjectUrl(client, o.url, `Activation of ${o.type} '${o.name}'`);
|
|
4965
|
+
}
|
|
4756
4966
|
const result = await activateBatch(client.http, client.safety, objects, activateOpts);
|
|
4757
4967
|
const names = objects.map((o) => o.name).join(', ');
|
|
4758
4968
|
const batchStatuses = buildBatchActivationStatuses(objects, result);
|
|
@@ -4809,9 +5019,21 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4809
5019
|
const groupLc = encodeURIComponent(group.toLowerCase());
|
|
4810
5020
|
objectUrl = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(name.toLowerCase())}`;
|
|
4811
5021
|
}
|
|
5022
|
+
else if (isServerDrivenObjectType(type)) {
|
|
5023
|
+
// Server-driven objects (8.16+): objectBasePath(<sdo>) throws, so route via the registry href.
|
|
5024
|
+
// Single-object activation only — SDO is not added to the batch resolver above (batch is
|
|
5025
|
+
// RAP-stack-oriented). The generic activate() endpoint handles SDO (verified: activate(DESD) → ok).
|
|
5026
|
+
objectUrl = serverDrivenObjectUrl(type, name);
|
|
5027
|
+
}
|
|
4812
5028
|
else {
|
|
4813
5029
|
objectUrl = objectUrlForType(type, name);
|
|
4814
5030
|
}
|
|
5031
|
+
// Enforce the allowedPackages ceiling against the object's REAL package before
|
|
5032
|
+
// activating — activation is a write-class state change (inactive draft → active
|
|
5033
|
+
// runtime version) and must honor the same package boundary as create/update/delete.
|
|
5034
|
+
// Fail-closed; no-op when allowedPackages is unrestricted. (security audit 2026-06)
|
|
5035
|
+
// SDO metadata only renders its packageRef under the blues Accept — thread it for SDO types.
|
|
5036
|
+
await enforceAllowedPackageForObjectUrl(client, objectUrl, `Activation of ${type} '${name}'`, isServerDrivenObjectType(type) ? serverDrivenBlueContentType(type) : undefined);
|
|
4815
5037
|
const result = await activate(client.http, client.safety, objectUrl, { ...activateOpts, name });
|
|
4816
5038
|
if (result.success) {
|
|
4817
5039
|
cachingLayer?.invalidate(type, name, 'all');
|
|
@@ -5125,6 +5347,29 @@ async function handleSAPDiagnose(client, args) {
|
|
|
5125
5347
|
const result = await runAtcCheck(client.http, client.safety, objectUrl, variant);
|
|
5126
5348
|
return textResult(JSON.stringify(result, null, 2));
|
|
5127
5349
|
}
|
|
5350
|
+
case 'cds_testcases': {
|
|
5351
|
+
// SAP-suggested ABAP Unit test cases for a CDS entity (CDS Test Double Framework).
|
|
5352
|
+
// The CDS name goes straight into the ?ddlsourceName= query param — no object URL.
|
|
5353
|
+
if (!name) {
|
|
5354
|
+
return errorResult('"name" (the CDS entity / DDLS source name) is required for "cds_testcases".');
|
|
5355
|
+
}
|
|
5356
|
+
// Discovery-gate: the endpoint exists only on SAP_BASIS 8.16+ (ABAP Platform 2025).
|
|
5357
|
+
// `false` = discovery loaded and the collection is absent (7.5x / 758) → clear message.
|
|
5358
|
+
// `undefined` = discovery not loaded → attempt and let a 404/400 surface normally.
|
|
5359
|
+
if (supportsCdsTestCases(client.http) === false) {
|
|
5360
|
+
return errorResult('CDS test-case scaffolding requires SAP_BASIS 8.16+ (ABAP Platform 2025 / S/4HANA 2025). ' +
|
|
5361
|
+
'This system does not expose /sap/bc/adt/aunit/dbtestdoubles/cds/testcases.');
|
|
5362
|
+
}
|
|
5363
|
+
const result = await getCdsTestCases(client.http, client.safety, name);
|
|
5364
|
+
const payload = {
|
|
5365
|
+
...result,
|
|
5366
|
+
hint: `Scaffold an ABAP Unit test class for ${result.cds}: ` +
|
|
5367
|
+
`cl_cds_test_environment=>create( i_for_entity = '${result.cds}' ) in class_setup, ` +
|
|
5368
|
+
'then implement one FOR TESTING method per case (insert_test_data for the doubled sources, ' +
|
|
5369
|
+
'assert with cl_abap_unit_assert). AI testdata/testmethod generation is not exposed.',
|
|
5370
|
+
};
|
|
5371
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
5372
|
+
}
|
|
5128
5373
|
case 'object_state': {
|
|
5129
5374
|
if (!name || !type)
|
|
5130
5375
|
return errorResult('"name" and "type" are required for "object_state" action.');
|
|
@@ -6143,6 +6388,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
6143
6388
|
const superPackage = String(args.superPackage ?? '').trim();
|
|
6144
6389
|
const softwareComponent = String(args.softwareComponent ?? '').trim();
|
|
6145
6390
|
const transportLayer = String(args.transportLayer ?? '').trim();
|
|
6391
|
+
const recordChanges = typeof args.recordChanges === 'boolean' ? args.recordChanges : undefined;
|
|
6146
6392
|
const transport = String(args.transport ?? '').trim();
|
|
6147
6393
|
if (!name)
|
|
6148
6394
|
return errorResult('"name" is required for create_package action.');
|
|
@@ -6201,7 +6447,9 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
6201
6447
|
superPackage: superPackage || undefined,
|
|
6202
6448
|
softwareComponent: softwareComponent || undefined,
|
|
6203
6449
|
transportLayer: transportLayer || undefined,
|
|
6450
|
+
recordChanges,
|
|
6204
6451
|
packageType,
|
|
6452
|
+
responsible: config.username,
|
|
6205
6453
|
});
|
|
6206
6454
|
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
6207
6455
|
// Hierarchy changed: invalidate any cached subtree that could contain
|
|
@@ -6269,6 +6517,12 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
6269
6517
|
}
|
|
6270
6518
|
objectUri = uriMatch[1];
|
|
6271
6519
|
}
|
|
6520
|
+
// SECURITY: gate the object's REAL package (resolved from objectUri via ADT
|
|
6521
|
+
// metadata), not the caller-supplied `oldPackage` — authorization must never
|
|
6522
|
+
// trust an attacker-controlled source-package string. This is the authoritative
|
|
6523
|
+
// source gate; the `checkPackage(oldPackage)` above is defense-in-depth only.
|
|
6524
|
+
// Fail-closed; no-op when allowedPackages is unrestricted. (security audit 2026-06)
|
|
6525
|
+
await enforceAllowedPackageForObjectUrl(client, objectUri, `change_package of ${objectName}`);
|
|
6272
6526
|
// Transport pre-flight for non-local target packages
|
|
6273
6527
|
let effectiveTransport = transport || undefined;
|
|
6274
6528
|
if (!effectiveTransport && newPackage.toUpperCase() !== '$TMP') {
|