arc-1 0.9.13 → 0.9.14

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.
@@ -916,9 +916,10 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
916
916
  return requestContext.run({ requestId: reqId, user, tool: toolName }, async () => {
917
917
  try {
918
918
  let result;
919
+ const cacheSecurity = buildCacheSecurityContext(authInfo, isPerUserClient);
919
920
  switch (toolName) {
920
921
  case 'SAPRead':
921
- result = await handleSAPRead(client, args, cachingLayer);
922
+ result = await handleSAPRead(client, args, cachingLayer, cacheSecurity);
922
923
  break;
923
924
  case 'SAPSearch':
924
925
  result = await handleSAPSearch(client, args);
@@ -927,10 +928,10 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
927
928
  result = await handleSAPQuery(client, args);
928
929
  break;
929
930
  case 'SAPWrite':
930
- result = await handleSAPWrite(client, args, config, cachingLayer);
931
+ result = await handleSAPWrite(client, args, config, cachingLayer, cacheSecurity);
931
932
  break;
932
933
  case 'SAPActivate':
933
- result = await handleSAPActivate(client, args, cachingLayer);
934
+ result = await handleSAPActivate(client, args, cachingLayer, cacheSecurity);
934
935
  break;
935
936
  case 'SAPNavigate':
936
937
  result = await handleSAPNavigate(client, args);
@@ -948,7 +949,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
948
949
  result = await handleSAPGit(client, args, authInfo);
949
950
  break;
950
951
  case 'SAPContext':
951
- result = await handleSAPContext(client, args, cachingLayer);
952
+ result = await handleSAPContext(client, args, cachingLayer, cacheSecurity);
952
953
  break;
953
954
  case 'SAPManage':
954
955
  result = await handleSAPManage(client, config, args, cachingLayer, isPerUserClient);
@@ -1065,7 +1066,47 @@ const VERSIONED_SOURCE_READ_TYPES = new Set([
1065
1066
  function inactiveTypeMatches(readType, inactiveType) {
1066
1067
  return (inactiveType.split('/')[0] ?? inactiveType).toUpperCase() === readType.toUpperCase();
1067
1068
  }
1068
- async function resolveVersionAndDraftInfo(client, cachingLayer, type, name, requestedVersion) {
1069
+ function nonEmptyString(value) {
1070
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
1071
+ }
1072
+ function resolveCacheUserKey(authInfo) {
1073
+ if (!authInfo)
1074
+ return undefined;
1075
+ const extra = (authInfo.extra ?? {});
1076
+ const issuerOrClient = nonEmptyString(extra.iss) ?? nonEmptyString(authInfo.clientId) ?? 'unknown-auth-source';
1077
+ const namespace = issuerOrClient.toLowerCase();
1078
+ const userName = nonEmptyString(extra.userName);
1079
+ if (userName)
1080
+ return `${namespace}:userName:${userName.toUpperCase()}`;
1081
+ const email = nonEmptyString(extra.email);
1082
+ if (email)
1083
+ return `${namespace}:email:${email.toLowerCase()}`;
1084
+ const sub = nonEmptyString(extra.sub);
1085
+ if (sub)
1086
+ return `${namespace}:sub:${sub}`;
1087
+ const preferredUsername = nonEmptyString(extra.preferred_username);
1088
+ if (preferredUsername)
1089
+ return `${namespace}:preferred_username:${preferredUsername.toLowerCase()}`;
1090
+ return undefined;
1091
+ }
1092
+ function buildCacheSecurityContext(authInfo, isPerUserClient) {
1093
+ if (!isPerUserClient)
1094
+ return { isPerUserClient: false };
1095
+ return {
1096
+ isPerUserClient: true,
1097
+ userKey: resolveCacheUserKey(authInfo),
1098
+ };
1099
+ }
1100
+ function inactiveListUserKey(client, cacheSecurity) {
1101
+ return cacheSecurity?.isPerUserClient ? cacheSecurity.userKey : client.username;
1102
+ }
1103
+ function invalidateInactiveList(cachingLayer, client, cacheSecurity) {
1104
+ cachingLayer?.inactiveLists.invalidate(inactiveListUserKey(client, cacheSecurity));
1105
+ }
1106
+ function contextCacheForDependencyPayloads(cachingLayer, cacheSecurity) {
1107
+ return cacheSecurity?.isPerUserClient ? undefined : cachingLayer;
1108
+ }
1109
+ async function resolveVersionAndDraftInfo(client, cachingLayer, type, name, requestedVersion, cacheSecurity) {
1069
1110
  if (!VERSIONED_SOURCE_READ_TYPES.has(type)) {
1070
1111
  return { effectiveVersion: requestedVersion === 'auto' ? 'active' : requestedVersion };
1071
1112
  }
@@ -1073,7 +1114,7 @@ async function resolveVersionAndDraftInfo(client, cachingLayer, type, name, requ
1073
1114
  if (cachingLayer || requestedVersion !== 'active') {
1074
1115
  try {
1075
1116
  const inactiveObjects = cachingLayer
1076
- ? await cachingLayer.inactiveLists.getOrFetch(client)
1117
+ ? await cachingLayer.inactiveLists.getOrFetch(client, inactiveListUserKey(client, cacheSecurity))
1077
1118
  : await client.getInactiveObjects();
1078
1119
  const upperName = name.toUpperCase();
1079
1120
  draft = inactiveObjects.find((object) => inactiveTypeMatches(type, object.type) && object.name.toUpperCase() === upperName);
@@ -1103,7 +1144,7 @@ function sourceVersionWarning(effectiveVersion, draft) {
1103
1144
  }
1104
1145
  return undefined;
1105
1146
  }
1106
- async function handleSAPRead(client, args, cachingLayer) {
1147
+ async function handleSAPRead(client, args, cachingLayer, cacheSecurity) {
1107
1148
  const type = normalizeObjectType(String(args.type ?? ''));
1108
1149
  const name = String(args.name ?? '');
1109
1150
  const requestedVersion = (args.version ?? 'active');
@@ -1126,10 +1167,10 @@ async function handleSAPRead(client, args, cachingLayer) {
1126
1167
  return textResult(JSON.stringify(sdo, null, 2));
1127
1168
  }
1128
1169
  if (args.force_refresh === true && cachingLayer && VERSIONED_SOURCE_READ_TYPES.has(type)) {
1129
- cachingLayer.inactiveLists.invalidate(client.username);
1170
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
1130
1171
  cachingLayer.invalidate(type, name, 'all');
1131
1172
  }
1132
- const { effectiveVersion, draft } = await resolveVersionAndDraftInfo(client, cachingLayer, type, name, requestedVersion);
1173
+ const { effectiveVersion, draft } = await resolveVersionAndDraftInfo(client, cachingLayer, type, name, requestedVersion, cacheSecurity);
1133
1174
  const versionWarning = sourceVersionWarning(effectiveVersion, draft);
1134
1175
  // Helper: get source with cache support, returns cache hit status
1135
1176
  const cachedGet = async (objType, objName, version, fetcher) => {
@@ -2580,6 +2621,13 @@ export function buildCreateXml(type, name, pkg, description, properties, languag
2580
2621
  description,
2581
2622
  package: pkg,
2582
2623
  messages: messages.length > 0 ? messages : undefined,
2624
+ // Thread the configured language into the body (same spirit as #343).
2625
+ // Live-verified on a4h 7.58: the MSAG handler keys T100.SPRSL by the
2626
+ // BODY adtcore:language — without it the messages are stored under a
2627
+ // BLANK language key (texts never resolve at runtime; ATC/SLIN flags
2628
+ // every number as missing). The sap-language URL param alone does NOT
2629
+ // prevent this.
2630
+ language: masterLanguage,
2583
2631
  };
2584
2632
  return buildMessageClassXml(params);
2585
2633
  }
@@ -3148,7 +3196,7 @@ async function enforceAllowedPackageForObjectUrl(client, objectUrl, label, accep
3148
3196
  * PUT; ABAP-specific pre-write steps (lint, RAP preflight, CDS guard) do not apply. Create leaves the
3149
3197
  * object inactive — callers follow with SAPActivate (never auto-activated).
3150
3198
  */
3151
- async function handleServerDrivenObjectWrite(client, action, type, name, args, cachingLayer) {
3199
+ async function handleServerDrivenObjectWrite(client, action, type, name, args, cachingLayer, cacheSecurity) {
3152
3200
  // Discovery gate — mirror handleSAPRead's server-driven branch.
3153
3201
  if (supportsServerDrivenObject(client.http, type) === false) {
3154
3202
  return errorResult(`SAPWrite type=${type} (server-driven object) requires SAP_BASIS 8.16+ (ABAP Platform 2025 / S/4HANA 2025). ` +
@@ -3159,7 +3207,7 @@ async function handleServerDrivenObjectWrite(client, action, type, name, args, c
3159
3207
  const blueAccept = serverDrivenBlueContentType(type);
3160
3208
  const invalidate = () => {
3161
3209
  cachingLayer?.invalidate(type, name, 'all');
3162
- cachingLayer?.inactiveLists.invalidate(client.username);
3210
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
3163
3211
  };
3164
3212
  // SDO source is AFF JSON (not ABAP) — validate it parses before any PUT.
3165
3213
  const validateSource = () => {
@@ -3222,7 +3270,7 @@ async function handleServerDrivenObjectWrite(client, action, type, name, args, c
3222
3270
  'Supported: create, update, delete (source is AFF JSON) — then SAPActivate to activate.');
3223
3271
  }
3224
3272
  }
3225
- async function handleSAPWrite(client, args, config, cachingLayer) {
3273
+ async function handleSAPWrite(client, args, config, cachingLayer, cacheSecurity) {
3226
3274
  const action = String(args.action ?? '');
3227
3275
  const type = normalizeWriteObjectType(String(args.type ?? ''));
3228
3276
  const name = String(args.name ?? '');
@@ -3262,7 +3310,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3262
3310
  // objectBasePath(<sdo>) throws, so this MUST come before the objectUrl computation. Mirrors the
3263
3311
  // server-driven branch in handleSAPRead.
3264
3312
  if (isServerDrivenObjectType(type)) {
3265
- return handleServerDrivenObjectWrite(client, action, type, name, args, cachingLayer);
3313
+ return handleServerDrivenObjectWrite(client, action, type, name, args, cachingLayer, cacheSecurity);
3266
3314
  }
3267
3315
  // For TABL update/delete/edit_method, the existing object may live at /tables/
3268
3316
  // (transparent) or /structures/ (DDIC structure). Resolve once via the client's
@@ -3333,7 +3381,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3333
3381
  const invalidateWrittenObject = (objType = type, objName = name) => {
3334
3382
  // Source cache is keyed by canonical type (SAPRead collapses TABL/DT, TABL/DS).
3335
3383
  cachingLayer?.invalidate(canonicalTablType(objType), objName, 'all');
3336
- cachingLayer?.inactiveLists.invalidate(client.username);
3384
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
3337
3385
  };
3338
3386
  // Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
3339
3387
  // Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
@@ -3351,7 +3399,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3351
3399
  // surgery call on a draft would splice active-version line ranges into inactive
3352
3400
  // source and silently corrupt the draft.
3353
3401
  async function fetchClassStructureAndMain(clsName) {
3354
- const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', clsName, 'auto');
3402
+ const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', clsName, 'auto', cacheSecurity);
3355
3403
  const structure = await client.getClassStructure(clsName, effectiveVersion);
3356
3404
  const main = cachingLayer
3357
3405
  ? (await cachingLayer.getSource('CLAS', clsName, (ifNoneMatch) => client.getClass(clsName, undefined, { ifNoneMatch, version: effectiveVersion }), { version: effectiveVersion })).source
@@ -3740,7 +3788,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3740
3788
  // splice against stale content (and frequently "method not found").
3741
3789
  // Use the standard inactive-list lookup to pick the right version —
3742
3790
  // same auto-resolution semantics SAPRead exposes via `version='auto'`.
3743
- const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', name, 'auto');
3791
+ const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', name, 'auto', cacheSecurity);
3744
3792
  const fetched = await client.getClass(name, resolvedInclude, { version: effectiveVersion });
3745
3793
  currentSource = stripIncludeHeader(fetched.source);
3746
3794
  // If the include itself has no draft (only MAIN does), SAP returns the
@@ -4604,7 +4652,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
4604
4652
  for (const o of writtenObjects) {
4605
4653
  cachingLayer?.invalidate(o.type, o.name, 'all');
4606
4654
  }
4607
- cachingLayer?.inactiveLists.invalidate(client.username);
4655
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
4608
4656
  }
4609
4657
  else {
4610
4658
  // Flip every written-but-not-yet-activated entry to 'failed', preserving the
@@ -4824,7 +4872,7 @@ async function runPreWriteSyntaxCheck(client, type, source, objectUrl, config, p
4824
4872
  }
4825
4873
  }
4826
4874
  // ─── SAPActivate Handler ─────────────────────────────────────────────
4827
- async function handleSAPActivate(client, args, cachingLayer) {
4875
+ async function handleSAPActivate(client, args, cachingLayer, cacheSecurity) {
4828
4876
  const action = String(args.action ?? 'activate');
4829
4877
  const name = String(args.name ?? '');
4830
4878
  const version = String(args.version ?? '0001');
@@ -4850,6 +4898,7 @@ async function handleSAPActivate(client, args, cachingLayer) {
4850
4898
  if (!name) {
4851
4899
  return errorResult('Missing required "name" parameter for publish_srvb action.');
4852
4900
  }
4901
+ await enforceAllowedPackageForObjectUrl(client, objectUrlForType('SRVB', name), `Publish of service binding '${name}'`, SERVICEBINDING_V2_CONTENT_TYPE);
4853
4902
  const serviceType = await resolveServiceType();
4854
4903
  const result = await publishServiceBinding(client.http, client.safety, name, version, serviceType);
4855
4904
  if (result.severity === 'ERROR') {
@@ -4885,6 +4934,7 @@ async function handleSAPActivate(client, args, cachingLayer) {
4885
4934
  if (!name) {
4886
4935
  return errorResult('Missing required "name" parameter for unpublish_srvb action.');
4887
4936
  }
4937
+ await enforceAllowedPackageForObjectUrl(client, objectUrlForType('SRVB', name), `Unpublish of service binding '${name}'`, SERVICEBINDING_V2_CONTENT_TYPE);
4888
4938
  const serviceType = await resolveServiceType();
4889
4939
  const result = await unpublishServiceBinding(client.http, client.safety, name, version, serviceType);
4890
4940
  if (result.severity === 'ERROR') {
@@ -4971,7 +5021,7 @@ async function handleSAPActivate(client, args, cachingLayer) {
4971
5021
  for (const object of objects) {
4972
5022
  cachingLayer?.invalidate(object.type, object.name, 'all');
4973
5023
  }
4974
- cachingLayer?.inactiveLists.invalidate(client.username);
5024
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
4975
5025
  return textResult(`Successfully activated ${objects.length} objects: ${names}.${statusDetails}`);
4976
5026
  }
4977
5027
  // On batch failure enrich with per-object inactive-version syntax errors —
@@ -5037,7 +5087,7 @@ async function handleSAPActivate(client, args, cachingLayer) {
5037
5087
  const result = await activate(client.http, client.safety, objectUrl, { ...activateOpts, name });
5038
5088
  if (result.success) {
5039
5089
  cachingLayer?.invalidate(type, name, 'all');
5040
- cachingLayer?.inactiveLists.invalidate(client.username);
5090
+ invalidateInactiveList(cachingLayer, client, cacheSecurity);
5041
5091
  return textResult(`Successfully activated ${type} ${name}.${formatActivationMessages(result)}`);
5042
5092
  }
5043
5093
  // On failure, try to enrich with the actual compiler errors from the inactive version —
@@ -6082,7 +6132,7 @@ function parseSiblingMaxCandidates(value) {
6082
6132
  const rounded = Math.trunc(parsed);
6083
6133
  return Math.min(Math.max(rounded, 1), HARD_MAX_SIBLING_MAX_CANDIDATES);
6084
6134
  }
6085
- async function handleSAPContext(client, args, cachingLayer) {
6135
+ async function handleSAPContext(client, args, cachingLayer, cacheSecurity) {
6086
6136
  const action = String(args.action ?? '');
6087
6137
  // action="impact" is DDLS-only on the server side — default the type so LLMs
6088
6138
  // don't have to supply it redundantly (and don't get a validation retry when
@@ -6099,6 +6149,10 @@ async function handleSAPContext(client, args, cachingLayer) {
6099
6149
  if (action === 'usages') {
6100
6150
  if (!name)
6101
6151
  return errorResult('"name" is required for usages action.');
6152
+ if (cacheSecurity?.isPerUserClient) {
6153
+ return errorResult('SAPContext(action="usages") is disabled under principal propagation because it reads the shared warmup index. ' +
6154
+ `Use SAPNavigate(action="references", type="${type || 'CLAS'}", name="${name}") for a live SAP-authorized lookup.`);
6155
+ }
6102
6156
  if (!cachingLayer) {
6103
6157
  return errorResult('Reverse dependency lookup requires object caching. Cache is disabled (ARC1_CACHE=none). ' +
6104
6158
  'Enable caching and run cache warmup to use this feature.');
@@ -6314,7 +6368,7 @@ async function handleSAPContext(client, args, cachingLayer) {
6314
6368
  }
6315
6369
  case 'DDLS': {
6316
6370
  const ddlSource = await cachedGet('DDLS', name, (ifNoneMatch) => client.getDdls(name, { ifNoneMatch }));
6317
- const cdsResult = await compressCdsContext(client, ddlSource, name, maxDeps, depth, cachingLayer);
6371
+ const cdsResult = await compressCdsContext(client, ddlSource, name, maxDeps, depth, contextCacheForDependencyPayloads(cachingLayer, cacheSecurity));
6318
6372
  return textResult(cdsResult.output);
6319
6373
  }
6320
6374
  default:
@@ -6322,8 +6376,9 @@ async function handleSAPContext(client, args, cachingLayer) {
6322
6376
  }
6323
6377
  }
6324
6378
  // Check dep graph cache — if source hash matches, return cached contracts
6325
- if (cachingLayer) {
6326
- const cachedGraph = cachingLayer.getCachedDepGraph(source);
6379
+ const dependencyPayloadCache = contextCacheForDependencyPayloads(cachingLayer, cacheSecurity);
6380
+ if (dependencyPayloadCache) {
6381
+ const cachedGraph = dependencyPayloadCache.getCachedDepGraph(source);
6327
6382
  if (cachedGraph) {
6328
6383
  const successful = cachedGraph.contracts.filter((c) => c.success);
6329
6384
  const failed = cachedGraph.contracts.filter((c) => !c.success);
@@ -6353,7 +6408,7 @@ async function handleSAPContext(client, args, cachingLayer) {
6353
6408
  const abaplintVersion = cachedFeatures?.abapRelease
6354
6409
  ? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
6355
6410
  : undefined;
6356
- const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion, cachingLayer);
6411
+ const result = await compressContext(client, source, name, type, maxDeps, depth, abaplintVersion, dependencyPayloadCache);
6357
6412
  return textResult(result.output);
6358
6413
  }
6359
6414
  function buildCdsUpstream(deps) {