arc-1 0.9.11 → 0.9.13

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 (79) hide show
  1. package/README.md +23 -30
  2. package/dist/adt/abapgit.d.ts +10 -0
  3. package/dist/adt/abapgit.d.ts.map +1 -1
  4. package/dist/adt/abapgit.js +18 -1
  5. package/dist/adt/abapgit.js.map +1 -1
  6. package/dist/adt/client.d.ts +6 -1
  7. package/dist/adt/client.d.ts.map +1 -1
  8. package/dist/adt/client.js +21 -5
  9. package/dist/adt/client.js.map +1 -1
  10. package/dist/adt/ddic-xml.d.ts +31 -0
  11. package/dist/adt/ddic-xml.d.ts.map +1 -1
  12. package/dist/adt/ddic-xml.js +33 -7
  13. package/dist/adt/ddic-xml.js.map +1 -1
  14. package/dist/adt/devtools.d.ts +22 -1
  15. package/dist/adt/devtools.d.ts.map +1 -1
  16. package/dist/adt/devtools.js +74 -0
  17. package/dist/adt/devtools.js.map +1 -1
  18. package/dist/adt/server-driven.d.ts +84 -0
  19. package/dist/adt/server-driven.d.ts.map +1 -0
  20. package/dist/adt/server-driven.js +207 -0
  21. package/dist/adt/server-driven.js.map +1 -0
  22. package/dist/adt/transport.d.ts +10 -2
  23. package/dist/adt/transport.d.ts.map +1 -1
  24. package/dist/adt/transport.js +56 -4
  25. package/dist/adt/transport.js.map +1 -1
  26. package/dist/adt/types.d.ts +48 -0
  27. package/dist/adt/types.d.ts.map +1 -1
  28. package/dist/adt/xml-parser.d.ts +8 -1
  29. package/dist/adt/xml-parser.d.ts.map +1 -1
  30. package/dist/adt/xml-parser.js +33 -0
  31. package/dist/adt/xml-parser.js.map +1 -1
  32. package/dist/authz/policy.d.ts.map +1 -1
  33. package/dist/authz/policy.js +1 -0
  34. package/dist/authz/policy.js.map +1 -1
  35. package/dist/extract-sap-cookies.d.ts +12 -1
  36. package/dist/extract-sap-cookies.d.ts.map +1 -1
  37. package/dist/extract-sap-cookies.js +3 -3
  38. package/dist/extract-sap-cookies.js.map +1 -1
  39. package/dist/handlers/intent.d.ts +25 -1
  40. package/dist/handlers/intent.d.ts.map +1 -1
  41. package/dist/handlers/intent.js +349 -68
  42. package/dist/handlers/intent.js.map +1 -1
  43. package/dist/handlers/schemas.d.ts +84 -45
  44. package/dist/handlers/schemas.d.ts.map +1 -1
  45. package/dist/handlers/schemas.js +100 -57
  46. package/dist/handlers/schemas.js.map +1 -1
  47. package/dist/handlers/tools.d.ts.map +1 -1
  48. package/dist/handlers/tools.js +131 -20
  49. package/dist/handlers/tools.js.map +1 -1
  50. package/dist/handlers/zod-errors.d.ts +2 -1
  51. package/dist/handlers/zod-errors.d.ts.map +1 -1
  52. package/dist/handlers/zod-errors.js +4 -2
  53. package/dist/handlers/zod-errors.js.map +1 -1
  54. package/dist/probe/catalog.d.ts.map +1 -1
  55. package/dist/probe/catalog.js +6 -0
  56. package/dist/probe/catalog.js.map +1 -1
  57. package/dist/server/audit.d.ts +18 -4
  58. package/dist/server/audit.d.ts.map +1 -1
  59. package/dist/server/audit.js.map +1 -1
  60. package/dist/server/http.d.ts +25 -2
  61. package/dist/server/http.d.ts.map +1 -1
  62. package/dist/server/http.js +67 -5
  63. package/dist/server/http.js.map +1 -1
  64. package/dist/server/oauth-state.d.ts +2 -0
  65. package/dist/server/oauth-state.d.ts.map +1 -1
  66. package/dist/server/oauth-state.js +5 -2
  67. package/dist/server/oauth-state.js.map +1 -1
  68. package/dist/server/server.d.ts +1 -1
  69. package/dist/server/server.js +2 -2
  70. package/dist/server/server.js.map +1 -1
  71. package/dist/server/stateless-client-store.d.ts +72 -4
  72. package/dist/server/stateless-client-store.d.ts.map +1 -1
  73. package/dist/server/stateless-client-store.js +143 -4
  74. package/dist/server/stateless-client-store.js.map +1 -1
  75. package/dist/server/xsuaa.d.ts +83 -0
  76. package/dist/server/xsuaa.d.ts.map +1 -1
  77. package/dist/server/xsuaa.js +2 -1
  78. package/dist/server/xsuaa.js.map +1 -1
  79. package/package.json +7 -4
@@ -9,13 +9,13 @@
9
9
  * responses. Internal details (stack traces, SAP XML) are NOT
10
10
  * leaked to the LLM — only user-friendly error messages.
11
11
  */
12
- import { checkRepo as abapGitCheckRepo, createBranch as abapGitCreateBranch, createRepo as abapGitCreateRepo, getExternalInfo as abapGitGetExternalInfo, listRepos as abapGitListRepos, pullRepo as abapGitPullRepo, pushRepo as abapGitPushRepo, stageRepo as abapGitStageRepo, switchBranch as abapGitSwitchBranch, unlinkRepo as abapGitUnlinkRepo, } from '../adt/abapgit.js';
12
+ import { checkRepo as abapGitCheckRepo, createBranch as abapGitCreateBranch, createRepo as abapGitCreateRepo, enforceRepoPackageAllowed as abapGitEnforceRepoPackage, getExternalInfo as abapGitGetExternalInfo, listRepos as abapGitListRepos, pullRepo as abapGitPullRepo, pushRepo as abapGitPushRepo, stageRepo as abapGitStageRepo, switchBranch as abapGitSwitchBranch, unlinkRepo as abapGitUnlinkRepo, } from '../adt/abapgit.js';
13
13
  import { buildSiblingExtensionFinding, classifyCdsImpact, deriveSiblingStem, isSiblingNameMatch, } from '../adt/cds-impact.js';
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
- let actionOrType = toolName === 'SAPRead'
830
- ? typeof args.type === 'string'
831
- ? args.type
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 args.searchType === 'string' &&
838
- args.searchType === 'tadir_lookup' &&
839
- typeof args.source === 'string') {
840
- const src = args.source.toLowerCase();
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 = normalizeTypeArgsForValidation(toolName, 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="DEVELOPER">
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="DEVELOPER">
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="DEVELOPER">
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="DEVELOPER">
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="DEVELOPER">
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="DEVELOPER">
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="DEVELOPER">
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="DEVELOPER">
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="DEVELOPER"
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="DEVELOPER">
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
- /** Normalize type fields before schema validation so slash/case aliases are accepted. */
2762
- function normalizeTypeArgsForValidation(toolName, args) {
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
- ...args,
2767
- type: normalizeObjectType(String(args.type ?? '')),
2768
- objectType: args.objectType === undefined ? undefined : normalizeObjectType(String(args.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
- ...args,
2774
- type: args.type === undefined ? undefined : normalizeWriteObjectType(String(args.type ?? '')),
2775
- objects: Array.isArray(args.objects)
2776
- ? args.objects.map((obj) => typeof obj === 'object' && obj !== null
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
- : args.objects,
2879
+ : cleaned.objects,
2783
2880
  };
2881
+ }
2784
2882
  case 'SAPActivate':
2785
2883
  return {
2786
- ...args,
2787
- type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
2788
- objects: Array.isArray(args.objects)
2789
- ? args.objects.map((obj) => typeof obj === 'object' && obj !== null
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
- : args.objects,
2893
+ : cleaned.objects,
2796
2894
  };
2797
2895
  case 'SAPSearch':
2798
2896
  return {
2799
- ...args,
2800
- objectType: args.objectType === undefined ? undefined : normalizeObjectType(String(args.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
- ...args,
2807
- type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
2904
+ ...cleaned,
2905
+ type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
2808
2906
  };
2809
2907
  case 'SAPDiagnose':
2810
2908
  return {
2811
- ...args,
2812
- type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
2909
+ ...cleaned,
2910
+ type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
2813
2911
  };
2814
2912
  case 'SAPContext':
2815
2913
  return {
2816
- ...args,
2817
- type: args.type === undefined ? undefined : normalizeObjectType(String(args.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
- ...args,
2828
- type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
2925
+ ...cleaned,
2926
+ type: cleaned.type === undefined ? undefined : normalizeObjectType(String(cleaned.type ?? '')),
2829
2927
  };
2830
2928
  default:
2831
- return args;
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
- if (client.safety.allowedPackages.length === 0)
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.');
@@ -5475,6 +5720,11 @@ async function handleSAPGit(client, args, _authInfo) {
5475
5720
  result = await gctsPullRepo(client.http, client.safety, repoId, String(args.commit ?? '').trim() || undefined);
5476
5721
  }
5477
5722
  else {
5723
+ // R9: a pull deserializes remote content into the repo's server-bound package, which is
5724
+ // NOT the caller-supplied `package` (abapGit ignores that for an existing repo). Gate the
5725
+ // real binding against the allowlist before writing.
5726
+ const repo = await loadAbapGitRepo(client, repoId);
5727
+ await abapGitEnforceRepoPackage(client.safety, repo.package, client.getPackageHierarchyResolver(), 'SAPGit(action="pull")');
5478
5728
  result = await abapGitPullRepo(client.http, client.safety, repoId, {
5479
5729
  ...(packageName ? { package: packageName } : {}),
5480
5730
  ...(url ? { url } : {}),
@@ -5489,6 +5739,9 @@ async function handleSAPGit(client, args, _authInfo) {
5489
5739
  if (!repoId)
5490
5740
  return errorResult('SAPGit(action="push") requires repoId.');
5491
5741
  const repo = await loadAbapGitRepo(client, repoId);
5742
+ // R9: push exports the repo's bound-package source to a remote git; gate that package
5743
+ // against the allowlist (the read-side mirror of the pull gate above).
5744
+ await abapGitEnforceRepoPackage(client.safety, repo.package, client.getPackageHierarchyResolver(), 'SAPGit(action="push")');
5492
5745
  const staging = Array.isArray(args.objects) && args.objects.length > 0
5493
5746
  ? { repoKey: repo.key, branchName: repo.branchName, objects: args.objects }
5494
5747
  : await abapGitStageRepo(client.http, client.safety, repo);
@@ -5713,8 +5966,24 @@ async function handleSAPTransport(client, args) {
5713
5966
  if (!id)
5714
5967
  return errorResult('Transport ID is required for "delete" action.');
5715
5968
  const recursive = Boolean(args.recursive ?? false);
5716
- await deleteTransport(client.http, client.safety, id, recursive);
5717
- return textResult(`Deleted transport request: ${id}${recursive ? ' (recursive)' : ''}`);
5969
+ const removeLockedObjects = Boolean(args.removeLockedObjects ?? false);
5970
+ try {
5971
+ await deleteTransport(client.http, client.safety, id, recursive, removeLockedObjects);
5972
+ }
5973
+ catch (e) {
5974
+ // ADT refuses to delete a request/task that still holds locked objects (e.g. a deleted
5975
+ // object's lingering record). Point the caller at removeLockedObjects instead of a raw [?/009].
5976
+ if (!removeLockedObjects && e instanceof Error && /locked objects/i.test(e.message)) {
5977
+ return errorResult(`${e.message}\n\nThe request still holds locked object(s). ` +
5978
+ `Retry with removeLockedObjects=true to strip them first:\n` +
5979
+ ` SAPTransport(action="delete", id="${id}", removeLockedObjects=true)`);
5980
+ }
5981
+ throw e;
5982
+ }
5983
+ const extras = [recursive ? 'recursive' : '', removeLockedObjects ? 'removed locked objects' : '']
5984
+ .filter(Boolean)
5985
+ .join(', ');
5986
+ return textResult(`Deleted transport request: ${id}${extras ? ` (${extras})` : ''}`);
5718
5987
  }
5719
5988
  case 'reassign': {
5720
5989
  const id = String(args.id ?? '');
@@ -5821,7 +6090,10 @@ async function handleSAPContext(client, args, cachingLayer) {
5821
6090
  const rawType = String(args.type ?? '');
5822
6091
  const type = normalizeObjectType(rawType || (action === 'impact' ? 'DDLS' : ''));
5823
6092
  const name = String(args.name ?? '');
5824
- const maxDeps = Number(args.maxDeps ?? 20);
6093
+ // Bound dependency fan-out: a huge maxDeps would fan out unbounded SAP fetches per level
6094
+ // (depth is already capped at 3). Clamp to [1, 100]; non-finite/<1 falls back to the default 20.
6095
+ const rawMaxDeps = Number(args.maxDeps ?? 20);
6096
+ const maxDeps = Number.isFinite(rawMaxDeps) && rawMaxDeps >= 1 ? Math.min(Math.floor(rawMaxDeps), 100) : 20;
5825
6097
  const depth = Math.min(Math.max(Number(args.depth ?? 1), 1), 3);
5826
6098
  // ─── Reverse dep lookup (pre-warmer only) ─────────────────────────
5827
6099
  if (action === 'usages') {
@@ -6143,6 +6415,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
6143
6415
  const superPackage = String(args.superPackage ?? '').trim();
6144
6416
  const softwareComponent = String(args.softwareComponent ?? '').trim();
6145
6417
  const transportLayer = String(args.transportLayer ?? '').trim();
6418
+ const recordChanges = typeof args.recordChanges === 'boolean' ? args.recordChanges : undefined;
6146
6419
  const transport = String(args.transport ?? '').trim();
6147
6420
  if (!name)
6148
6421
  return errorResult('"name" is required for create_package action.');
@@ -6201,7 +6474,9 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
6201
6474
  superPackage: superPackage || undefined,
6202
6475
  softwareComponent: softwareComponent || undefined,
6203
6476
  transportLayer: transportLayer || undefined,
6477
+ recordChanges,
6204
6478
  packageType,
6479
+ responsible: config.username,
6205
6480
  });
6206
6481
  await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport, undefined, cachedFeatures?.abapRelease);
6207
6482
  // Hierarchy changed: invalidate any cached subtree that could contain
@@ -6269,6 +6544,12 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
6269
6544
  }
6270
6545
  objectUri = uriMatch[1];
6271
6546
  }
6547
+ // SECURITY: gate the object's REAL package (resolved from objectUri via ADT
6548
+ // metadata), not the caller-supplied `oldPackage` — authorization must never
6549
+ // trust an attacker-controlled source-package string. This is the authoritative
6550
+ // source gate; the `checkPackage(oldPackage)` above is defense-in-depth only.
6551
+ // Fail-closed; no-op when allowedPackages is unrestricted. (security audit 2026-06)
6552
+ await enforceAllowedPackageForObjectUrl(client, objectUri, `change_package of ${objectName}`);
6272
6553
  // Transport pre-flight for non-local target packages
6273
6554
  let effectiveTransport = transport || undefined;
6274
6555
  if (!effectiveTransport && newPackage.toUpperCase() !== '$TMP') {