arc-1 0.9.13 → 0.9.15
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 +17 -4
- package/dist/adt/class-structure.d.ts +1 -1
- package/dist/adt/class-structure.js +1 -1
- package/dist/adt/client.d.ts +16 -3
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +40 -35
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/ddic-xml.d.ts +9 -0
- package/dist/adt/ddic-xml.d.ts.map +1 -1
- package/dist/adt/ddic-xml.js +49 -53
- package/dist/adt/ddic-xml.js.map +1 -1
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +46 -15
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/fm-signature.d.ts +1 -1
- package/dist/adt/fm-signature.js +1 -1
- package/dist/adt/rap-generate.js +1 -1
- package/dist/adt/rap-generate.js.map +1 -1
- package/dist/adt/rap-handlers.d.ts +1 -1
- package/dist/adt/rap-handlers.js +1 -1
- package/dist/adt/refactoring.d.ts.map +1 -1
- package/dist/adt/refactoring.js +11 -14
- package/dist/adt/refactoring.js.map +1 -1
- package/dist/adt/server-driven.d.ts +46 -2
- package/dist/adt/server-driven.d.ts.map +1 -1
- package/dist/adt/server-driven.js +13 -3
- package/dist/adt/server-driven.js.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 +57 -49
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts +1 -1
- package/dist/authz/policy.js +1 -1
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/inactive-list-cache.d.ts +4 -4
- package/dist/cache/inactive-list-cache.d.ts.map +1 -1
- package/dist/cache/inactive-list-cache.js +19 -12
- package/dist/cache/inactive-list-cache.js.map +1 -1
- package/dist/cache/memory.d.ts +1 -0
- package/dist/cache/memory.d.ts.map +1 -1
- package/dist/cache/memory.js +3 -0
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/sqlite.d.ts +1 -0
- package/dist/cache/sqlite.d.ts.map +1 -1
- package/dist/cache/sqlite.js +3 -0
- package/dist/cache/sqlite.js.map +1 -1
- package/dist/cache/warmup.d.ts.map +1 -1
- package/dist/cache/warmup.js +85 -38
- package/dist/cache/warmup.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/context/compressor.d.ts.map +1 -1
- package/dist/context/compressor.js +22 -13
- package/dist/context/compressor.js.map +1 -1
- package/dist/context/contract.d.ts.map +1 -1
- package/dist/context/contract.js +4 -3
- package/dist/context/contract.js.map +1 -1
- package/dist/context/deps.d.ts.map +1 -1
- package/dist/context/deps.js +3 -2
- package/dist/context/deps.js.map +1 -1
- package/dist/context/grep.d.ts +3 -1
- package/dist/context/grep.d.ts.map +1 -1
- package/dist/context/grep.js +83 -1
- package/dist/context/grep.js.map +1 -1
- package/dist/context/method-surgery.d.ts.map +1 -1
- package/dist/context/method-surgery.js +3 -2
- package/dist/context/method-surgery.js.map +1 -1
- package/dist/handlers/activate.d.ts +25 -0
- package/dist/handlers/activate.d.ts.map +1 -0
- package/dist/handlers/activate.js +334 -0
- package/dist/handlers/activate.js.map +1 -0
- package/dist/handlers/cache-security.d.ts +22 -0
- package/dist/handlers/cache-security.d.ts.map +1 -0
- package/dist/handlers/cache-security.js +51 -0
- package/dist/handlers/cache-security.js.map +1 -0
- package/dist/handlers/cds-hints.d.ts +26 -0
- package/dist/handlers/cds-hints.d.ts.map +1 -0
- package/dist/handlers/cds-hints.js +380 -0
- package/dist/handlers/cds-hints.js.map +1 -0
- package/dist/handlers/context.d.ts +10 -0
- package/dist/handlers/context.d.ts.map +1 -0
- package/dist/handlers/context.js +344 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/diagnose.d.ts +8 -0
- package/dist/handlers/diagnose.d.ts.map +1 -0
- package/dist/handlers/diagnose.js +274 -0
- package/dist/handlers/diagnose.js.map +1 -0
- package/dist/handlers/dispatch.d.ts +39 -0
- package/dist/handlers/dispatch.d.ts.map +1 -0
- package/dist/handlers/dispatch.js +640 -0
- package/dist/handlers/dispatch.js.map +1 -0
- package/dist/handlers/feature-cache.d.ts +26 -0
- package/dist/handlers/feature-cache.d.ts.map +1 -0
- package/dist/handlers/feature-cache.js +45 -0
- package/dist/handlers/feature-cache.js.map +1 -0
- package/dist/handlers/git.d.ts +9 -0
- package/dist/handlers/git.d.ts.map +1 -0
- package/dist/handlers/git.js +227 -0
- package/dist/handlers/git.js.map +1 -0
- package/dist/handlers/lint.d.ts +9 -0
- package/dist/handlers/lint.d.ts.map +1 -0
- package/dist/handlers/lint.js +82 -0
- package/dist/handlers/lint.js.map +1 -0
- package/dist/handlers/manage.d.ts +10 -0
- package/dist/handlers/manage.d.ts.map +1 -0
- package/dist/handlers/manage.js +375 -0
- package/dist/handlers/manage.js.map +1 -0
- package/dist/handlers/navigate.d.ts +8 -0
- package/dist/handlers/navigate.d.ts.map +1 -0
- package/dist/handlers/navigate.js +188 -0
- package/dist/handlers/navigate.js.map +1 -0
- package/dist/handlers/object-types.d.ts +103 -0
- package/dist/handlers/object-types.d.ts.map +1 -0
- package/dist/handlers/object-types.js +476 -0
- package/dist/handlers/object-types.js.map +1 -0
- package/dist/handlers/query.d.ts +7 -0
- package/dist/handlers/query.d.ts.map +1 -0
- package/dist/handlers/query.js +190 -0
- package/dist/handlers/query.js.map +1 -0
- package/dist/handlers/read.d.ts +18 -0
- package/dist/handlers/read.d.ts.map +1 -0
- package/dist/handlers/read.js +581 -0
- package/dist/handlers/read.js.map +1 -0
- package/dist/handlers/schemas.d.ts +28 -26
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +17 -153
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/search.d.ts +24 -0
- package/dist/handlers/search.d.ts.map +1 -0
- package/dist/handlers/search.js +208 -0
- package/dist/handlers/search.js.map +1 -0
- package/dist/handlers/shared.d.ts +19 -0
- package/dist/handlers/shared.d.ts.map +1 -0
- package/dist/handlers/shared.js +23 -0
- package/dist/handlers/shared.js.map +1 -0
- package/dist/handlers/tool-registry.d.ts +44 -0
- package/dist/handlers/tool-registry.d.ts.map +1 -0
- package/dist/handlers/tool-registry.js +152 -0
- package/dist/handlers/tool-registry.js.map +1 -0
- package/dist/handlers/tools.d.ts +9 -0
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +40 -141
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/transport.d.ts +8 -0
- package/dist/handlers/transport.d.ts.map +1 -0
- package/dist/handlers/transport.js +281 -0
- package/dist/handlers/transport.js.map +1 -0
- package/dist/handlers/write/class-surgery.d.ts +18 -0
- package/dist/handlers/write/class-surgery.d.ts.map +1 -0
- package/dist/handlers/write/class-surgery.js +366 -0
- package/dist/handlers/write/class-surgery.js.map +1 -0
- package/dist/handlers/write/context.d.ts +43 -0
- package/dist/handlers/write/context.d.ts.map +1 -0
- package/dist/handlers/write/context.js +5 -0
- package/dist/handlers/write/context.js.map +1 -0
- package/dist/handlers/write/create.d.ts +8 -0
- package/dist/handlers/write/create.d.ts.map +1 -0
- package/dist/handlers/write/create.js +603 -0
- package/dist/handlers/write/create.js.map +1 -0
- package/dist/handlers/write/rap.d.ts +8 -0
- package/dist/handlers/write/rap.d.ts.map +1 -0
- package/dist/handlers/write/rap.js +235 -0
- package/dist/handlers/write/rap.js.map +1 -0
- package/dist/handlers/write/update-delete.d.ts +8 -0
- package/dist/handlers/write/update-delete.d.ts.map +1 -0
- package/dist/handlers/write/update-delete.js +182 -0
- package/dist/handlers/write/update-delete.js.map +1 -0
- package/dist/handlers/write-helpers.d.ts +155 -0
- package/dist/handlers/write-helpers.d.ts.map +1 -0
- package/dist/handlers/write-helpers.js +859 -0
- package/dist/handlers/write-helpers.js.map +1 -0
- package/dist/handlers/write.d.ts +16 -0
- package/dist/handlers/write.d.ts.map +1 -0
- package/dist/handlers/write.js +210 -0
- package/dist/handlers/write.js.map +1 -0
- package/dist/handlers/zod-errors.d.ts +1 -1
- package/dist/handlers/zod-errors.js +1 -1
- package/dist/lint/abaplint-config-cache.d.ts +5 -0
- package/dist/lint/abaplint-config-cache.d.ts.map +1 -0
- package/dist/lint/abaplint-config-cache.js +29 -0
- package/dist/lint/abaplint-config-cache.js.map +1 -0
- package/dist/lint/config-builder.d.ts.map +1 -1
- package/dist/lint/config-builder.js +3 -4
- package/dist/lint/config-builder.js.map +1 -1
- package/dist/lint/lint.d.ts +1 -1
- package/dist/lint/lint.d.ts.map +1 -1
- package/dist/lint/lint.js +3 -2
- package/dist/lint/lint.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +22 -8
- package/dist/server/config.js.map +1 -1
- package/dist/server/mcp-rate-limit.d.ts +1 -1
- package/dist/server/mcp-rate-limit.js +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +3 -2
- package/dist/server/server.js.map +1 -1
- package/package.json +15 -10
- package/dist/handlers/intent.d.ts +0 -144
- package/dist/handlers/intent.d.ts.map +0 -1
- package/dist/handlers/intent.js +0 -6782
- package/dist/handlers/intent.js.map +0 -1
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handleToolCall — the intent dispatcher for ARC-1.
|
|
3
|
+
*
|
|
4
|
+
* Runs the per-call pipeline (rate-limit → scope → deny-actions → Zod → route to one of the 12
|
|
5
|
+
* tool handlers → audit) and owns the LLM-facing error-formatting tree.
|
|
6
|
+
*/
|
|
7
|
+
import { AdtApiError, AdtNetworkError, AdtSafetyError, classifySapDomainError } from '../adt/errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Scope required for each tool.
|
|
10
|
+
*
|
|
11
|
+
* Scope enforcement is ADDITIVE to the safety system:
|
|
12
|
+
* - Safety system (allowWrites, allowedPackages, etc.) gates operations at the ADT client level
|
|
13
|
+
* - Scopes gate operations at the MCP tool level (only enforced when authInfo is present)
|
|
14
|
+
* - Both must pass for an operation to succeed
|
|
15
|
+
*
|
|
16
|
+
* A user with `write` scope but `allowWrites=false` in config still can't write.
|
|
17
|
+
*
|
|
18
|
+
* Scope lookup and implication rules are defined in `src/authz/policy.ts` (ACTION_POLICY,
|
|
19
|
+
* getActionPolicy, hasRequiredScope). This module routes through them.
|
|
20
|
+
*/
|
|
21
|
+
import { getActionPolicy, hasRequiredScope as hasScopeHelper } from '../authz/policy.js';
|
|
22
|
+
import { sanitizeArgs } from '../server/audit.js';
|
|
23
|
+
import { generateRequestId, requestContext } from '../server/context.js';
|
|
24
|
+
import { logger } from '../server/logger.js';
|
|
25
|
+
import { resolveRateLimitUserKey } from '../server/mcp-rate-limit.js';
|
|
26
|
+
import { handleSAPActivate } from './activate.js';
|
|
27
|
+
import { buildCacheSecurityContext } from './cache-security.js';
|
|
28
|
+
import { handleSAPContext } from './context.js';
|
|
29
|
+
import { handleSAPDiagnose } from './diagnose.js';
|
|
30
|
+
import { cachedFeatures } from './feature-cache.js';
|
|
31
|
+
import { handleSAPGit } from './git.js';
|
|
32
|
+
import { expandHyperfocusedArgs } from './hyperfocused.js';
|
|
33
|
+
import { handleSAPLint } from './lint.js';
|
|
34
|
+
import { handleSAPManage } from './manage.js';
|
|
35
|
+
import { handleSAPNavigate } from './navigate.js';
|
|
36
|
+
import { canonicalTablType, normalizeObjectType, normalizeTypeArgsForValidation } from './object-types.js';
|
|
37
|
+
import { handleSAPQuery } from './query.js';
|
|
38
|
+
import { handleSAPRead } from './read.js';
|
|
39
|
+
import { getToolSchema } from './schemas.js';
|
|
40
|
+
import { handleSAPSearch } from './search.js';
|
|
41
|
+
import { errorResult, hasSqlParserSignature } from './shared.js';
|
|
42
|
+
import { handleSAPTransport } from './transport.js';
|
|
43
|
+
import { handleSAPWrite } from './write.js';
|
|
44
|
+
import { formatZodError } from './zod-errors.js';
|
|
45
|
+
/**
|
|
46
|
+
* Back-compat re-export of a tool→scope map derived from ACTION_POLICY.
|
|
47
|
+
* New code should use `getActionPolicy(tool, action)` directly.
|
|
48
|
+
*/
|
|
49
|
+
export const TOOL_SCOPES = Object.fromEntries([
|
|
50
|
+
'SAPRead',
|
|
51
|
+
'SAPSearch',
|
|
52
|
+
'SAPQuery',
|
|
53
|
+
'SAPGit',
|
|
54
|
+
'SAPNavigate',
|
|
55
|
+
'SAPContext',
|
|
56
|
+
'SAPLint',
|
|
57
|
+
'SAPDiagnose',
|
|
58
|
+
'SAPWrite',
|
|
59
|
+
'SAPActivate',
|
|
60
|
+
'SAPManage',
|
|
61
|
+
'SAPTransport',
|
|
62
|
+
].map((t) => [t, getActionPolicy(t)?.scope ?? 'read']));
|
|
63
|
+
/**
|
|
64
|
+
* Check if authInfo has the required scope, routing through policy.hasRequiredScope.
|
|
65
|
+
*/
|
|
66
|
+
export function hasRequiredScope(authInfo, requiredScope) {
|
|
67
|
+
return hasScopeHelper(authInfo.scopes, requiredScope);
|
|
68
|
+
}
|
|
69
|
+
const DDIC_SAVE_HINT_TYPES = new Set(['TABL', 'DDLS', 'DCLS', 'BDEF', 'SRVD', 'SRVB', 'DDLX', 'DOMA', 'DTEL']);
|
|
70
|
+
function getWriteInfrastructureHint(err, tool, args) {
|
|
71
|
+
if (tool !== 'SAPWrite')
|
|
72
|
+
return undefined;
|
|
73
|
+
const action = String(args.action ?? '').toLowerCase();
|
|
74
|
+
if (!['create', 'update', 'batch_create', 'edit_method', 'delete'].includes(action))
|
|
75
|
+
return undefined;
|
|
76
|
+
// These failures happen around ADT session management, often after SAP has
|
|
77
|
+
// already accepted a mutation. They need cleanup guidance, not DDIC syntax hints.
|
|
78
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}\n${err.path}`.toLowerCase();
|
|
79
|
+
const failedDuringCsrfFetch = err.path.includes('/sap/bc/adt/core/discovery') || combined.includes('no csrf token');
|
|
80
|
+
const failedDuringUnlock = combined.includes('_action=unlock');
|
|
81
|
+
const serviceRoutingFailure = combined.includes('service cannot be reached');
|
|
82
|
+
if (!failedDuringCsrfFetch && !failedDuringUnlock && !serviceRoutingFailure)
|
|
83
|
+
return undefined;
|
|
84
|
+
return ('SAP ADT write/session infrastructure failed, not a DDIC source save failure. ' +
|
|
85
|
+
'The object may have been partially created or changed before the session failed; verify with SAPRead/SAPSearch, ' +
|
|
86
|
+
'wait briefly, then retry cleanup. If an edit lock remains, release it in ADT/SM12 or ask Basis to clear it.');
|
|
87
|
+
}
|
|
88
|
+
/** Format error messages with LLM-friendly remediation hints */
|
|
89
|
+
function formatErrorForLLM(err, message, tool, args, config) {
|
|
90
|
+
const base = buildBaseErrorMessage(err, message, tool, args, config);
|
|
91
|
+
// Handler-attached remediation hints (e.g., CDS delete blocker list) always
|
|
92
|
+
// appear last so the message reads "what happened → diagnostics → how to fix".
|
|
93
|
+
if (err instanceof AdtApiError && err.extraHint && !base.includes(err.extraHint)) {
|
|
94
|
+
return `${base}\n\n${err.extraHint}`;
|
|
95
|
+
}
|
|
96
|
+
return base;
|
|
97
|
+
}
|
|
98
|
+
function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
99
|
+
if (err instanceof AdtApiError) {
|
|
100
|
+
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
101
|
+
const enriched = enrichWithSapDetails(err, message);
|
|
102
|
+
const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
|
|
103
|
+
// Pass the detected SAP_BASIS release so the 423 lock-handle hint can specialize
|
|
104
|
+
// (< 7.51 → point at abapfs_extensions; see issue #293). cachedFeatures is set by the
|
|
105
|
+
// startup probe; config.abapRelease is the manual SAP_ABAP_RELEASE override fallback.
|
|
106
|
+
const abapRelease = cachedFeatures?.abapRelease ?? config.abapRelease;
|
|
107
|
+
const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path, abapRelease);
|
|
108
|
+
if (classification) {
|
|
109
|
+
const transactionLine = classification.transaction ? `\nSAP Transaction: ${classification.transaction}` : '';
|
|
110
|
+
return `${enriched}\n\nHint: ${classification.hint}${transactionLine}`;
|
|
111
|
+
}
|
|
112
|
+
if (err.isNotFound) {
|
|
113
|
+
const diagnosticsHint = buildDiagnosticsNotFoundHint(tool, args);
|
|
114
|
+
if (diagnosticsHint) {
|
|
115
|
+
return `${enriched}\n\nHint: ${diagnosticsHint}`;
|
|
116
|
+
}
|
|
117
|
+
const name = String(args.name ?? '');
|
|
118
|
+
const type = String(args.type ?? '');
|
|
119
|
+
return `${enriched}\n\nHint: Object "${name}" (type ${type}) was not found. Use SAPSearch with query "${name}" to verify the name exists and check the correct type.`;
|
|
120
|
+
}
|
|
121
|
+
if (err.isUnauthorized || err.isForbidden) {
|
|
122
|
+
if (config.cookieFile || config.cookieString) {
|
|
123
|
+
return (`${enriched}\n\n` +
|
|
124
|
+
'Hint: SAP cookies have expired. Ask the user to re-extract cookies ' +
|
|
125
|
+
'with `arc1-cli extract-cookies`. The next SAP call after extraction ' +
|
|
126
|
+
'will automatically reload the fresh cookies — no restart needed.');
|
|
127
|
+
}
|
|
128
|
+
return `${enriched}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
|
|
129
|
+
}
|
|
130
|
+
// Transport / corrNr specific hints
|
|
131
|
+
const transportHint = getTransportHint(err);
|
|
132
|
+
if (transportHint) {
|
|
133
|
+
return `${enriched}\n\nHint: ${transportHint}`;
|
|
134
|
+
}
|
|
135
|
+
if (tool === 'SAPRead' && argType === 'TABLE_CONTENTS' && err.statusCode === 400) {
|
|
136
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}`;
|
|
137
|
+
if (hasSqlParserSignature(combined)) {
|
|
138
|
+
return (`${enriched}\n\nHint: TABLE_CONTENTS sqlFilter must be a condition expression only ` +
|
|
139
|
+
'(no WHERE, no SELECT, no semicolon). Examples: ' +
|
|
140
|
+
`sqlFilter="MANDT = '100'" or sqlFilter="MATNR LIKE 'Z%'".`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (tool === 'SAPRead' && argType === 'TABLE_QUERY' && err.statusCode === 400) {
|
|
144
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}`;
|
|
145
|
+
if (/is invalid here|due to grammar/i.test(combined)) {
|
|
146
|
+
return (`${enriched}\n\nHint: TABLE_QUERY parser error — check field names match the actual column names ` +
|
|
147
|
+
'exposed by the table/CDS view (use SAPRead(type="DDLS", include="elements") to inspect CDS view fields). ' +
|
|
148
|
+
'Also verify value formats (e.g. FiscalPeriod is C(2,0) so use "01" not "001").');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const behaviorPoolHint = getBehaviorPoolSaveFailureHint(err, args);
|
|
152
|
+
if (behaviorPoolHint) {
|
|
153
|
+
return `${enriched}\n\nHint: ${behaviorPoolHint}`;
|
|
154
|
+
}
|
|
155
|
+
const writeInfrastructureHint = getWriteInfrastructureHint(err, tool, args);
|
|
156
|
+
if (writeInfrastructureHint) {
|
|
157
|
+
return `${enriched}\n\nHint: ${writeInfrastructureHint}`;
|
|
158
|
+
}
|
|
159
|
+
// Save hint — applies to create/update/batch_create/edit_method, not delete.
|
|
160
|
+
// Delete failures on DDIC types have different remediation (dependency resolution, not annotation fixes).
|
|
161
|
+
const action = String(args.action ?? '').toLowerCase();
|
|
162
|
+
const isSaveAction = action === '' ||
|
|
163
|
+
action === 'create' ||
|
|
164
|
+
action === 'update' ||
|
|
165
|
+
action === 'batch_create' ||
|
|
166
|
+
action === 'edit_method';
|
|
167
|
+
if ((err.statusCode === 400 || err.statusCode === 409) && DDIC_SAVE_HINT_TYPES.has(argType) && isSaveAction) {
|
|
168
|
+
return (`${enriched}\n\nHint: DDIC save failed. Check the diagnostic details above for specific field or annotation errors. ` +
|
|
169
|
+
'Common fixes: add missing @AbapCatalog annotations, fix field type names, check key field definitions.');
|
|
170
|
+
}
|
|
171
|
+
// Server errors (500, 502, 503, etc.)
|
|
172
|
+
if (err.isServerError) {
|
|
173
|
+
// Detect syntax errors in dependent objects (e.g., BDEF syntax errors blocking SRVB activation)
|
|
174
|
+
const syntaxMatch = err.message.match(/[Ss]yntax error in program (\S+)/);
|
|
175
|
+
if (syntaxMatch) {
|
|
176
|
+
const program = syntaxMatch[1].replace(/=+\w*$/, ''); // Strip "====BD" padding
|
|
177
|
+
return `${enriched}\n\nHint: A dependent object has syntax errors that block this operation. The program "${program}" has syntax errors — fix those first, then retry. Use SAPRead to inspect the object, or SAPDiagnose(action="dumps") for details.`;
|
|
178
|
+
}
|
|
179
|
+
return `${enriched}\n\nHint: SAP application server error (${err.statusCode}). This is often transient — wait 10-30 seconds and retry. If the error persists, check SAPDiagnose(action="dumps") for short dumps, or verify the SAP system is responding via SAPRead(type="SYSTEM").`;
|
|
180
|
+
}
|
|
181
|
+
return enriched;
|
|
182
|
+
}
|
|
183
|
+
if (err instanceof AdtSafetyError) {
|
|
184
|
+
const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
|
|
185
|
+
if (tool === 'SAPRead' && argType === 'TABLE_CONTENTS') {
|
|
186
|
+
return (`${message}\n\nHint: TABLE_CONTENTS is blocked by safety configuration or missing data scope. ` +
|
|
187
|
+
'Set SAP_ALLOW_DATA_PREVIEW=true at the server level and, in authenticated HTTP mode, ' +
|
|
188
|
+
'ensure the token includes data (or sql) scope.');
|
|
189
|
+
}
|
|
190
|
+
return message;
|
|
191
|
+
}
|
|
192
|
+
if (err instanceof AdtNetworkError) {
|
|
193
|
+
if (tool === 'SAPRead' && String(args.type ?? '').toUpperCase() === 'SYSTEM') {
|
|
194
|
+
return (`${message}\n\nHint: Connectivity probe failed. Fix connectivity first, then retry ` +
|
|
195
|
+
'SAPRead(type="SYSTEM") before running any batch or parallel tool calls.');
|
|
196
|
+
}
|
|
197
|
+
return (`${message}\n\nHint: Cannot reach the SAP system. Run SAPRead(type="SYSTEM") once as a connectivity ` +
|
|
198
|
+
'probe before retrying batch/parallel calls.');
|
|
199
|
+
}
|
|
200
|
+
return message;
|
|
201
|
+
}
|
|
202
|
+
function buildDiagnosticsNotFoundHint(tool, args) {
|
|
203
|
+
if (tool !== 'SAPDiagnose')
|
|
204
|
+
return undefined;
|
|
205
|
+
const action = String(args.action ?? '');
|
|
206
|
+
const id = String(args.id ?? '').trim();
|
|
207
|
+
const detailUrl = String(args.detailUrl ?? '').trim();
|
|
208
|
+
if (action === 'dumps' && id) {
|
|
209
|
+
return `Dump ID "${id}" was not found. Re-list dumps with SAPDiagnose(action="dumps", maxResults=50), then retry with a fresh ID from that list.`;
|
|
210
|
+
}
|
|
211
|
+
if (action === 'traces' && id) {
|
|
212
|
+
return `Trace ID "${id}" was not found. Re-list traces with SAPDiagnose(action="traces") and retry using an existing trace ID.`;
|
|
213
|
+
}
|
|
214
|
+
if (action === 'gateway_errors' && (detailUrl || id)) {
|
|
215
|
+
return 'Gateway error detail was not found. Re-list SAPDiagnose(action="gateway_errors") and reuse a current detailUrl from the list output.';
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
function buildAuditResultPreview(toolName, args, fullText) {
|
|
220
|
+
const maxLen = 500;
|
|
221
|
+
const truncate = (value) => (value.length > maxLen ? `${value.slice(0, maxLen)}...` : value);
|
|
222
|
+
if (toolName !== 'SAPDiagnose')
|
|
223
|
+
return truncate(fullText);
|
|
224
|
+
const action = String(args.action ?? '');
|
|
225
|
+
const isDetailDump = action === 'dumps' && Boolean(args.id);
|
|
226
|
+
const isDetailGateway = action === 'gateway_errors' && (Boolean(args.detailUrl) || (Boolean(args.id) && Boolean(args.errorType)));
|
|
227
|
+
if (!isDetailDump && !isDetailGateway)
|
|
228
|
+
return truncate(fullText);
|
|
229
|
+
try {
|
|
230
|
+
const payload = JSON.parse(fullText);
|
|
231
|
+
if (isDetailDump) {
|
|
232
|
+
const sections = payload.sections && typeof payload.sections === 'object' ? payload.sections : {};
|
|
233
|
+
const compact = {
|
|
234
|
+
id: payload.id,
|
|
235
|
+
error: payload.error,
|
|
236
|
+
program: payload.program,
|
|
237
|
+
user: payload.user,
|
|
238
|
+
timestamp: payload.timestamp,
|
|
239
|
+
selectedSectionIds: payload.selectedSectionIds,
|
|
240
|
+
sections: Object.fromEntries(Object.entries(sections).map(([key, value]) => {
|
|
241
|
+
if (typeof value === 'string')
|
|
242
|
+
return [key, `[omitted ${value.length} chars]`];
|
|
243
|
+
return [key, '[omitted]'];
|
|
244
|
+
})),
|
|
245
|
+
formattedText: typeof payload.formattedText === 'string' ? `[omitted ${payload.formattedText.length} chars]` : undefined,
|
|
246
|
+
};
|
|
247
|
+
return truncate(JSON.stringify(compact));
|
|
248
|
+
}
|
|
249
|
+
if (isDetailGateway && payload.sourceCode && typeof payload.sourceCode === 'object') {
|
|
250
|
+
const sourceCode = payload.sourceCode;
|
|
251
|
+
const lines = Array.isArray(sourceCode.lines) ? sourceCode.lines.length : 0;
|
|
252
|
+
const compact = {
|
|
253
|
+
type: payload.type,
|
|
254
|
+
shortText: payload.shortText,
|
|
255
|
+
transactionId: payload.transactionId,
|
|
256
|
+
username: payload.username,
|
|
257
|
+
dateTime: payload.dateTime,
|
|
258
|
+
sourceCode: `[omitted ${lines} lines]`,
|
|
259
|
+
callStackCount: Array.isArray(payload.callStack) ? payload.callStack.length : 0,
|
|
260
|
+
};
|
|
261
|
+
return truncate(JSON.stringify(compact));
|
|
262
|
+
}
|
|
263
|
+
return truncate(JSON.stringify(payload));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return truncate(fullText);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Enrich error message with additional SAP XML diagnostic detail (extra messages, properties) */
|
|
270
|
+
function enrichWithSapDetails(err, message) {
|
|
271
|
+
if (!err.responseBody)
|
|
272
|
+
return message;
|
|
273
|
+
const extraMessages = AdtApiError.extractAllMessages(err.responseBody);
|
|
274
|
+
const props = AdtApiError.extractProperties(err.responseBody);
|
|
275
|
+
const parts = [message];
|
|
276
|
+
if (extraMessages.length > 0) {
|
|
277
|
+
parts.push(`\nAdditional detail:\n${extraMessages.map((m) => ` - ${m}`).join('\n')}`);
|
|
278
|
+
}
|
|
279
|
+
const ddicDiagnostics = AdtApiError.formatDdicDiagnostics(err.responseBody);
|
|
280
|
+
if (ddicDiagnostics) {
|
|
281
|
+
parts.push(ddicDiagnostics);
|
|
282
|
+
// Skip raw Properties dump — DDIC diagnostics already include the structured
|
|
283
|
+
// T100KEY details (message ID, number, variables, line). Showing both would
|
|
284
|
+
// triplicate the same information.
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Surface line/column info from properties if present (non-DDIC errors only)
|
|
288
|
+
const lineInfo = props.LINE || props['T100KEY-NO'];
|
|
289
|
+
if (lineInfo || Object.keys(props).length > 0) {
|
|
290
|
+
const propStr = Object.entries(props)
|
|
291
|
+
.slice(0, 5) // Limit to avoid overwhelming output
|
|
292
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
293
|
+
.join(', ');
|
|
294
|
+
if (propStr)
|
|
295
|
+
parts.push(`Properties: ${propStr}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return parts.join('\n');
|
|
299
|
+
}
|
|
300
|
+
/** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
|
|
301
|
+
function getTransportHint(err) {
|
|
302
|
+
const body = (err.responseBody ?? '').toLowerCase();
|
|
303
|
+
// Use the clean SAP error message, NOT err.message which includes the URL path.
|
|
304
|
+
// The URL path contains `corrNr=<id>` when a transport IS provided, causing false positives
|
|
305
|
+
// if we check for "corrnr" in the full message string.
|
|
306
|
+
const cleanMsg = AdtApiError.extractCleanMessage(err.responseBody ?? '').toLowerCase();
|
|
307
|
+
const combined = `${cleanMsg} ${body}`;
|
|
308
|
+
// Missing or invalid transport/correction number
|
|
309
|
+
if (combined.includes('correction number') ||
|
|
310
|
+
combined.includes('corrnr') ||
|
|
311
|
+
(combined.includes('transport request') &&
|
|
312
|
+
(combined.includes('missing') || combined.includes('required') || combined.includes('invalid')))) {
|
|
313
|
+
return 'A transport/correction number is required but was not provided or is invalid. Provide an explicit "transport" parameter with a valid transport request ID, or check SE09 in SAP GUI that an open transport exists for your user and target package.';
|
|
314
|
+
}
|
|
315
|
+
// Transport not found or not modifiable
|
|
316
|
+
if (combined.includes('e070') ||
|
|
317
|
+
(combined.includes('transport') &&
|
|
318
|
+
(combined.includes('not found') || combined.includes('does not exist') || combined.includes('not modifiable')))) {
|
|
319
|
+
return 'The specified transport request was not found or is not modifiable. Verify the transport ID in SE09, ensure it is not yet released, and that it belongs to the correct user and target package.';
|
|
320
|
+
}
|
|
321
|
+
// Package / transport layer mismatch
|
|
322
|
+
if (combined.includes('transport layer') ||
|
|
323
|
+
(combined.includes('package') &&
|
|
324
|
+
combined.includes('transport') &&
|
|
325
|
+
(combined.includes('mismatch') || combined.includes('not assigned') || combined.includes('no transport layer')))) {
|
|
326
|
+
return 'The target package has no transport layer or a transport layer mismatch. Check that the package is configured for transport in SE80/TDEVC, or use a local package ($TMP) if no transport is needed.';
|
|
327
|
+
}
|
|
328
|
+
// Authorization for transport operations
|
|
329
|
+
if (combined.includes('s_transprt') ||
|
|
330
|
+
(combined.includes('transport') && (combined.includes('no authorization') || combined.includes('not authorized')))) {
|
|
331
|
+
return 'The SAP user lacks transport authorization (S_TRANSPRT). Contact your SAP basis administrator to grant the required transport permissions.';
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
function inferBdefNameFromBehaviorPoolSource(source) {
|
|
336
|
+
const match = source.match(/\bfor\s+behavior\s+of\s+([A-Za-z_][\w/]+)/i);
|
|
337
|
+
return match?.[1];
|
|
338
|
+
}
|
|
339
|
+
function getBehaviorPoolSaveFailureHint(err, args) {
|
|
340
|
+
const type = normalizeObjectType(String(args.type ?? ''));
|
|
341
|
+
if (type !== 'CLAS')
|
|
342
|
+
return undefined;
|
|
343
|
+
const name = String(args.name ?? '');
|
|
344
|
+
const source = String(args.source ?? '');
|
|
345
|
+
const clean = AdtApiError.extractCleanMessage(err.responseBody ?? '').toLowerCase();
|
|
346
|
+
const body = (err.responseBody ?? '').toLowerCase();
|
|
347
|
+
const isGenericSaveFailure = clean.includes('an error occured during the save operation') ||
|
|
348
|
+
clean.includes('an error occurred during the save operation') ||
|
|
349
|
+
body.includes('an error occured during the save operation') ||
|
|
350
|
+
body.includes('an error occurred during the save operation');
|
|
351
|
+
if (!isGenericSaveFailure)
|
|
352
|
+
return undefined;
|
|
353
|
+
const looksLikeBehaviorPool = /\bfor\s+behavior\s+of\b/i.test(source) || /^zbp_/i.test(name) || /^ybp_/i.test(name);
|
|
354
|
+
if (!looksLikeBehaviorPool)
|
|
355
|
+
return undefined;
|
|
356
|
+
const inferredBdef = inferBdefNameFromBehaviorPoolSource(source);
|
|
357
|
+
const bdefHint = inferredBdef ? `, bdefName="${inferredBdef}"` : ', bdefName="<interface_bdef_name>"';
|
|
358
|
+
return (`Behavior-pool class save failed on handler declarations. Use ` +
|
|
359
|
+
`SAPWrite(action="scaffold_rap_handlers", type="CLAS", name="${name}"${bdefHint}) ` +
|
|
360
|
+
`to list missing RAP handler signatures, then rerun with autoApply=true to inject declarations. ` +
|
|
361
|
+
`If SAP still rejects the full-class write, use ADT quick-fix to stamp signatures and continue with SAPWrite(action="edit_method").`);
|
|
362
|
+
}
|
|
363
|
+
function classifyError(err) {
|
|
364
|
+
if (err instanceof AdtApiError) {
|
|
365
|
+
const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path);
|
|
366
|
+
return classification ? `AdtApiError:${classification.category}` : 'AdtApiError';
|
|
367
|
+
}
|
|
368
|
+
if (err instanceof AdtNetworkError)
|
|
369
|
+
return 'AdtNetworkError';
|
|
370
|
+
if (err instanceof AdtSafetyError)
|
|
371
|
+
return 'AdtSafetyError';
|
|
372
|
+
if (err instanceof Error)
|
|
373
|
+
return err.constructor.name;
|
|
374
|
+
return 'Unknown';
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Handle an MCP tool call.
|
|
378
|
+
*
|
|
379
|
+
* @param authInfo - Authenticated user context from MCP SDK (XSUAA/OIDC/API key).
|
|
380
|
+
* When present, scope enforcement is active. When absent (stdio, no auth),
|
|
381
|
+
* all tools are allowed (backward compatibility).
|
|
382
|
+
* @param server - MCP Server instance for elicitation support.
|
|
383
|
+
*/
|
|
384
|
+
export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient, mcpRateLimiter) {
|
|
385
|
+
const reqId = generateRequestId();
|
|
386
|
+
const start = Date.now();
|
|
387
|
+
// Build user context for audit logging
|
|
388
|
+
const user = authInfo?.extra?.userName;
|
|
389
|
+
const clientId = authInfo?.clientId;
|
|
390
|
+
// Emit tool_call_start audit event
|
|
391
|
+
logger.emitAudit({
|
|
392
|
+
timestamp: new Date().toISOString(),
|
|
393
|
+
level: 'info',
|
|
394
|
+
event: 'tool_call_start',
|
|
395
|
+
requestId: reqId,
|
|
396
|
+
user,
|
|
397
|
+
clientId,
|
|
398
|
+
tool: toolName,
|
|
399
|
+
args: sanitizeArgs(args),
|
|
400
|
+
});
|
|
401
|
+
// ─── Layer 2: per-user MCP tool-call rate limit ─────────────────────
|
|
402
|
+
// Applied immediately so we don't waste any work on denied calls. Stdio mode
|
|
403
|
+
// (no authInfo) is exempt — there's no user identity to key on. On denial we
|
|
404
|
+
// return an MCP tool error (not HTTP 429) so the LLM client surfaces it as a
|
|
405
|
+
// tool failure and the agent loop backs off via its own retry policy.
|
|
406
|
+
// See docs_page/rate-limiting.md (Layer 2). Cost weighting per tool is deferred
|
|
407
|
+
// to v2 — every consume call counts as one point.
|
|
408
|
+
if (mcpRateLimiter && authInfo) {
|
|
409
|
+
// Walks the most-specific identity claim first (userName → email → sub →
|
|
410
|
+
// preferred_username → clientId) so OIDC users sharing one `azp` clientId
|
|
411
|
+
// don't collapse into a single bucket. See resolveRateLimitUserKey.
|
|
412
|
+
const userKey = resolveRateLimitUserKey(authInfo);
|
|
413
|
+
const decision = await mcpRateLimiter.consume(userKey, toolName);
|
|
414
|
+
if (!decision.allowed) {
|
|
415
|
+
const retryAfter = Math.ceil(decision.retryAfterMs / 1000);
|
|
416
|
+
logger.emitAudit({
|
|
417
|
+
timestamp: new Date().toISOString(),
|
|
418
|
+
level: 'warn',
|
|
419
|
+
event: 'mcp_rate_limited',
|
|
420
|
+
requestId: reqId,
|
|
421
|
+
clientId,
|
|
422
|
+
user: userKey,
|
|
423
|
+
tool: toolName,
|
|
424
|
+
limitPerMinute: decision.limitPerMinute,
|
|
425
|
+
retryAfterMs: decision.retryAfterMs,
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: 'text',
|
|
431
|
+
text: JSON.stringify({
|
|
432
|
+
error: 'rate_limited',
|
|
433
|
+
retryAfter,
|
|
434
|
+
message: `Rate limit exceeded (${decision.limitPerMinute}/min per user). Retry after ${retryAfter} seconds.`,
|
|
435
|
+
}),
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
isError: true,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Unified scope enforcement via ACTION_POLICY — routes through action/type-aware lookup.
|
|
443
|
+
// For SAPRead, the policy key is Tool.{type}; for other action-bearing tools, Tool.{action};
|
|
444
|
+
// for tools without an action/type enum (SAPSearch, SAPQuery), the tool-level default applies.
|
|
445
|
+
// For SAPSearch.tadir_lookup with source='db'|'both', synthesize a sub-action key so the
|
|
446
|
+
// sql-scoped policy entry kicks in (otherwise viewer-only profiles could piggyback on the
|
|
447
|
+
// ADT info-system route to issue freestyle SQL).
|
|
448
|
+
//
|
|
449
|
+
// SECURITY (privilege-escalation hardening): the scope key is derived from the SAME
|
|
450
|
+
// normalized value the handler ultimately dispatches on. `normalizeTypeArgsForValidation`
|
|
451
|
+
// upper-cases + slash-collapses `type` and coerces non-string inputs via String(), so a
|
|
452
|
+
// caller cannot evade the per-type scope gate by sending a value that misses the policy key
|
|
453
|
+
// here yet is canonicalized into a privileged type just before Zod runs. Two such bypasses
|
|
454
|
+
// existed when this lookup read the RAW `args`: an array (`type: ["TABLE_CONTENTS"]` —
|
|
455
|
+
// typeof "object" → undefined key → base `read`) and a lowercase string
|
|
456
|
+
// (`type: "table_contents"` — no `SAPRead.table_contents` key → base `read`), both of which
|
|
457
|
+
// were then normalized into the data-scoped `TABLE_CONTENTS` for the handler. Normalizing
|
|
458
|
+
// first closes the array, case, and slash-form variants in one place (and keeps the
|
|
459
|
+
// SAP_DENY_ACTIONS match below consistent with the canonical form). The normalized object is
|
|
460
|
+
// reused for Zod validation below so canonicalization happens exactly once.
|
|
461
|
+
// Runs BEFORE Zod validation so scope errors don't leak schema details to unauthorized callers.
|
|
462
|
+
const normalizedArgs = normalizeTypeArgsForValidation(toolName, args);
|
|
463
|
+
const rawScopeKey = toolName === 'SAPRead' ? normalizedArgs.type : normalizedArgs.action;
|
|
464
|
+
let actionOrType = rawScopeKey === undefined || rawScopeKey === null || rawScopeKey === '' ? undefined : String(rawScopeKey);
|
|
465
|
+
if (toolName === 'SAPSearch' &&
|
|
466
|
+
typeof normalizedArgs.searchType === 'string' &&
|
|
467
|
+
normalizedArgs.searchType === 'tadir_lookup' &&
|
|
468
|
+
typeof normalizedArgs.source === 'string') {
|
|
469
|
+
const src = normalizedArgs.source.toLowerCase();
|
|
470
|
+
if (src === 'db' || src === 'both') {
|
|
471
|
+
actionOrType = `tadir_lookup_${src}`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const policy = getActionPolicy(toolName, actionOrType);
|
|
475
|
+
if (authInfo && policy) {
|
|
476
|
+
if (!hasRequiredScope(authInfo, policy.scope)) {
|
|
477
|
+
logger.emitAudit({
|
|
478
|
+
timestamp: new Date().toISOString(),
|
|
479
|
+
level: 'warn',
|
|
480
|
+
event: 'auth_scope_denied',
|
|
481
|
+
requestId: reqId,
|
|
482
|
+
user,
|
|
483
|
+
clientId,
|
|
484
|
+
tool: toolName,
|
|
485
|
+
requiredScope: policy.scope,
|
|
486
|
+
availableScopes: authInfo.scopes,
|
|
487
|
+
});
|
|
488
|
+
const actionLabel = actionOrType
|
|
489
|
+
? `${toolName}(${toolName === 'SAPRead' ? 'type' : 'action'}="${actionOrType}")`
|
|
490
|
+
: toolName;
|
|
491
|
+
return errorResult(`Insufficient scope: '${policy.scope}' required for ${actionLabel}. Your scopes: [${authInfo.scopes.join(', ')}]`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Server-level denyActions (SAP_DENY_ACTIONS) — blocks before any per-user scope allows it.
|
|
495
|
+
const { isActionDenied } = await import('../server/deny-actions.js');
|
|
496
|
+
if (isActionDenied(toolName, actionOrType, config.denyActions ?? [])) {
|
|
497
|
+
logger.emitAudit({
|
|
498
|
+
timestamp: new Date().toISOString(),
|
|
499
|
+
level: 'warn',
|
|
500
|
+
event: 'safety_blocked',
|
|
501
|
+
requestId: reqId,
|
|
502
|
+
user,
|
|
503
|
+
clientId,
|
|
504
|
+
operation: `${toolName}${actionOrType ? `.${actionOrType}` : ''}`,
|
|
505
|
+
reason: 'Action denied by SAP_DENY_ACTIONS',
|
|
506
|
+
});
|
|
507
|
+
return errorResult(`Action '${toolName}${actionOrType ? `.${actionOrType}` : ''}' is denied by server policy (SAP_DENY_ACTIONS).`);
|
|
508
|
+
}
|
|
509
|
+
// Validate tool arguments with Zod schema (runs AFTER scope + deny check).
|
|
510
|
+
const isBtp = config.systemType === 'btp';
|
|
511
|
+
const schema = getToolSchema(toolName, isBtp);
|
|
512
|
+
if (schema) {
|
|
513
|
+
// Reuse the normalized args computed for the scope-key derivation above —
|
|
514
|
+
// re-normalizing would be redundant (the transform is idempotent) and risks
|
|
515
|
+
// the two paths drifting.
|
|
516
|
+
args = normalizedArgs;
|
|
517
|
+
const parsed = schema.safeParse(args);
|
|
518
|
+
if (!parsed.success) {
|
|
519
|
+
const validationError = formatZodError(parsed.error, toolName);
|
|
520
|
+
logger.emitAudit({
|
|
521
|
+
timestamp: new Date().toISOString(),
|
|
522
|
+
level: 'warn',
|
|
523
|
+
event: 'safety_blocked',
|
|
524
|
+
requestId: reqId,
|
|
525
|
+
user,
|
|
526
|
+
clientId,
|
|
527
|
+
operation: toolName,
|
|
528
|
+
reason: 'Input validation failed',
|
|
529
|
+
});
|
|
530
|
+
return errorResult(validationError);
|
|
531
|
+
}
|
|
532
|
+
args = parsed.data;
|
|
533
|
+
}
|
|
534
|
+
// Run within request context so HTTP-level logs get the requestId
|
|
535
|
+
return requestContext.run({ requestId: reqId, user, tool: toolName }, async () => {
|
|
536
|
+
try {
|
|
537
|
+
let result;
|
|
538
|
+
const cacheSecurity = buildCacheSecurityContext(authInfo, isPerUserClient);
|
|
539
|
+
switch (toolName) {
|
|
540
|
+
case 'SAPRead':
|
|
541
|
+
result = await handleSAPRead(client, args, cachingLayer, cacheSecurity);
|
|
542
|
+
break;
|
|
543
|
+
case 'SAPSearch':
|
|
544
|
+
result = await handleSAPSearch(client, args);
|
|
545
|
+
break;
|
|
546
|
+
case 'SAPQuery':
|
|
547
|
+
result = await handleSAPQuery(client, args);
|
|
548
|
+
break;
|
|
549
|
+
case 'SAPWrite':
|
|
550
|
+
result = await handleSAPWrite(client, args, config, cachingLayer, cacheSecurity);
|
|
551
|
+
break;
|
|
552
|
+
case 'SAPActivate':
|
|
553
|
+
result = await handleSAPActivate(client, args, cachingLayer, cacheSecurity);
|
|
554
|
+
break;
|
|
555
|
+
case 'SAPNavigate':
|
|
556
|
+
result = await handleSAPNavigate(client, args);
|
|
557
|
+
break;
|
|
558
|
+
case 'SAPLint':
|
|
559
|
+
result = await handleSAPLint(client, args, config);
|
|
560
|
+
break;
|
|
561
|
+
case 'SAPDiagnose':
|
|
562
|
+
result = await handleSAPDiagnose(client, args);
|
|
563
|
+
break;
|
|
564
|
+
case 'SAPTransport':
|
|
565
|
+
result = await handleSAPTransport(client, args);
|
|
566
|
+
break;
|
|
567
|
+
case 'SAPGit':
|
|
568
|
+
result = await handleSAPGit(client, args, authInfo);
|
|
569
|
+
break;
|
|
570
|
+
case 'SAPContext':
|
|
571
|
+
result = await handleSAPContext(client, args, cachingLayer, cacheSecurity);
|
|
572
|
+
break;
|
|
573
|
+
case 'SAPManage':
|
|
574
|
+
result = await handleSAPManage(client, config, args, cachingLayer, isPerUserClient);
|
|
575
|
+
break;
|
|
576
|
+
case 'SAP': {
|
|
577
|
+
// Hyperfocused mode: route to the appropriate handler
|
|
578
|
+
const expanded = expandHyperfocusedArgs(args);
|
|
579
|
+
if ('error' in expanded) {
|
|
580
|
+
result = errorResult(expanded.error);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
// Delegate to the real handler (recursive call, but with the mapped tool name)
|
|
584
|
+
// The concrete tool/action policy is enforced by the recursive call.
|
|
585
|
+
result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer, isPerUserClient);
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
default:
|
|
589
|
+
result = errorResult(`Unknown tool: ${toolName}`);
|
|
590
|
+
}
|
|
591
|
+
const durationMs = Date.now() - start;
|
|
592
|
+
const fullText = result.content.map((c) => c.text).join('');
|
|
593
|
+
const resultSize = fullText.length;
|
|
594
|
+
const resultPreview = buildAuditResultPreview(toolName, args, fullText);
|
|
595
|
+
logger.emitAudit({
|
|
596
|
+
timestamp: new Date().toISOString(),
|
|
597
|
+
level: result.isError ? 'error' : 'info',
|
|
598
|
+
event: 'tool_call_end',
|
|
599
|
+
requestId: reqId,
|
|
600
|
+
user,
|
|
601
|
+
clientId,
|
|
602
|
+
tool: toolName,
|
|
603
|
+
durationMs,
|
|
604
|
+
status: result.isError ? 'error' : 'success',
|
|
605
|
+
errorMessage: result.isError ? result.content[0]?.text : undefined,
|
|
606
|
+
errorClass: result.isError ? 'result-path' : undefined,
|
|
607
|
+
resultSize,
|
|
608
|
+
resultPreview,
|
|
609
|
+
});
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
614
|
+
const durationMs = Date.now() - start;
|
|
615
|
+
logger.emitAudit({
|
|
616
|
+
timestamp: new Date().toISOString(),
|
|
617
|
+
level: 'error',
|
|
618
|
+
event: 'tool_call_end',
|
|
619
|
+
requestId: reqId,
|
|
620
|
+
user,
|
|
621
|
+
clientId,
|
|
622
|
+
tool: toolName,
|
|
623
|
+
durationMs,
|
|
624
|
+
status: 'error',
|
|
625
|
+
errorClass: classifyError(err),
|
|
626
|
+
errorMessage: message,
|
|
627
|
+
});
|
|
628
|
+
return errorResult(formatErrorForLLM(err, message, toolName, args, config));
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// ─── Individual Tool Handlers ────────────────────────────────────────
|
|
633
|
+
/** Check if the connected system is BTP ABAP Environment */
|
|
634
|
+
/** Return whether the SAP ADT discovery feed advertises the /sap/bc/adt/ddic/tables
|
|
635
|
+
* collection (the transparent-table editor endpoint). Absent on NW 7.50/7.51 —
|
|
636
|
+
* SAP added it in NW 7.52 along with the new database-table editor. When the
|
|
637
|
+
* discovery cache is empty (e.g. probe never ran, tests that bypass SAPManage),
|
|
638
|
+
* returns `undefined` so callers can decide whether to default-allow.
|
|
639
|
+
* See issue #285. */
|
|
640
|
+
//# sourceMappingURL=dispatch.js.map
|