@linkup-ai/abap-ai 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ MCP (Model Context Protocol) Server that connects [Claude Code](https://claude.a
4
4
 
5
5
  Once configured, Claude can read, write, create, activate, search, refactor, test and deploy ABAP objects — including RAP/Fiori stacks — directly from your editor without any manual SAP GUI interaction.
6
6
 
7
- **55 tools** covering the full ABAP development lifecycle, powered by a **curated knowledge base** with 49 reference guides on ABAP, CDS, RAP, Fiori and CAP.
7
+ **55 tools** covering the full ABAP development lifecycle, powered by a **curated knowledge base** with 50 reference guides on ABAP, CDS, RAP, Fiori and CAP.
8
8
 
9
9
  ---
10
10
 
@@ -132,11 +132,11 @@ Once configured, Claude can read, write, create, activate, search, refactor, tes
132
132
 
133
133
  The knowledge base is the core differentiator of this MCP server. It provides Claude with **curated, LLM-optimized reference material** (70% code examples, 30% rules and anti-patterns) so that generated code follows best practices out of the box.
134
134
 
135
- **49 guides** organized in 4 domains:
135
+ **50 guides** organized in 4 domains:
136
136
 
137
137
  | Domain | Topics | Examples |
138
138
  |--------|--------|----------|
139
- | **ABAP Core** | 25 | Internal tables, ABAP SQL, RAP (EML, draft, feature control, numbering, unmanaged), OO, clean code, performance, unit testing, cloud development |
139
+ | **ABAP Core** | 26 | Internal tables, ABAP SQL, RAP (EML, draft, feature control, numbering, unmanaged), OO, enhancements/BAdIs, clean code, performance, unit testing, cloud development |
140
140
  | **ABAP CDS** | 6 | Annotations, associations, access control, expressions, functions, metadata extensions |
141
141
  | **Fiori / UI5** | 11 | Fiori Elements, annotations, value lists, side effects, XML views, controllers, routing, data binding, fragments, manifest, deployment |
142
142
  | **CAP (Node.js)** | 7 | CDL syntax, CQL queries, service definitions, event handlers, authentication, Fiori integration, deployment |
@@ -242,7 +242,7 @@ See [docs/CLI_GUIDE.md](docs/CLI_GUIDE.md) for the full step-by-step guide.
242
242
  | Command | Description |
243
243
  |---------|-------------|
244
244
  | `abap-ai init` | Interactive wizard to configure SAP system connections |
245
- | `abap-ai activate <KEY>` | Activate product license (BYOK or All-in-One) |
245
+ | `abap-ai activate <KEY>` | Activate product license |
246
246
  | `abap-ai status` | Show license, systems (with connection test), usage metrics and knowledge base |
247
247
  | `abap-ai systems` | List configured SAP systems |
248
248
  | `abap-ai remove <name>` | Remove a SAP system from mcp.json |
@@ -49,19 +49,38 @@ function createHttpClient() {
49
49
  const http = createHttpClient();
50
50
  exports.http = http;
51
51
  // ─── Sessão CSRF ──────────────────────────────────────────────────
52
+ /** Máximo de tentativas para obter CSRF token */
53
+ const CSRF_MAX_RETRIES = 3;
54
+ /** Intervalo entre tentativas de CSRF (ms) */
55
+ const CSRF_RETRY_DELAY = 1000;
52
56
  async function ensureSession() {
53
57
  if (session)
54
58
  return session.csrfToken;
55
- const response = await http.get("/discovery", {
56
- headers: { "X-CSRF-Token": "Fetch", Accept: "application/xml" },
57
- validateStatus: (status) => status < 500, // 406 é esperado mas traz o CSRF token
58
- });
59
- const token = response.headers["x-csrf-token"];
60
- if (!token) {
61
- throw new AdtError("Falha ao obter CSRF token do SAP ADT. Verifique se o serviço /sap/bc/adt/discovery está ativo.", 0, "/discovery");
59
+ let lastError;
60
+ for (let attempt = 1; attempt <= CSRF_MAX_RETRIES; attempt++) {
61
+ try {
62
+ const response = await http.get("/discovery", {
63
+ headers: { "X-CSRF-Token": "Fetch", Accept: "application/xml" },
64
+ validateStatus: (status) => status < 500, // 406 é esperado mas traz o CSRF token
65
+ });
66
+ const token = response.headers["x-csrf-token"];
67
+ if (token) {
68
+ session = { csrfToken: token };
69
+ return token;
70
+ }
71
+ lastError = new AdtError("CSRF token não retornado pelo SAP ADT.", 0, "/discovery");
72
+ }
73
+ catch (error) {
74
+ lastError = error;
75
+ }
76
+ // Retry com backoff linear (1s, 2s, 3s)
77
+ if (attempt < CSRF_MAX_RETRIES) {
78
+ await new Promise((r) => setTimeout(r, CSRF_RETRY_DELAY * attempt));
79
+ }
62
80
  }
63
- session = { csrfToken: token };
64
- return token;
81
+ throw lastError instanceof AdtError
82
+ ? lastError
83
+ : new AdtError("Falha ao obter CSRF token do SAP ADT após 3 tentativas. Verifique se o serviço /sap/bc/adt/discovery está ativo.", 0, "/discovery");
65
84
  }
66
85
  function resetSession() {
67
86
  session = null;
@@ -35,12 +35,36 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.readLicense = readLicense;
37
37
  exports.activate = activate;
38
+ const crypto = __importStar(require("crypto"));
38
39
  const fs = __importStar(require("fs"));
39
40
  const path = __importStar(require("path"));
40
41
  const LICENSE_DIR = path.join(process.env.HOME || "~", ".abap-ai");
41
42
  const LICENSE_PATH = path.join(LICENSE_DIR, "license.json");
42
- // TODO: Substituir pela URL real quando o license server existir
43
- const LICENSE_API_URL = "https://api.lkpabap.ai/v1/license/validate";
43
+ // ── Beta key validation (offline, HMAC-based) ───────────────────────
44
+ // Em produção será substituído por fetch ao license server (api.lkpabap.ai)
45
+ const BETA_SECRET = "lkp-beta-2026-xK9mQ3vR";
46
+ const PLAN_MAP = { ST: "starter", PR: "pro", EN: "enterprise" };
47
+ function validateBetaKey(key) {
48
+ const upper = key.toUpperCase();
49
+ const match = upper.match(/^LK-([A-Z]{2})([A-Z]{2})-([A-Z0-9]{4})-([A-Z0-9]{4})$/);
50
+ if (!match)
51
+ return null;
52
+ const [, planCode, modeCode, rand, checksum] = match;
53
+ const plan = PLAN_MAP[planCode];
54
+ if (!plan || modeCode !== "BK")
55
+ return null;
56
+ // Recalcula HMAC e compara com o checksum da chave
57
+ const payload = `${planCode}${modeCode}-${rand}`;
58
+ const expected = crypto
59
+ .createHmac("sha256", BETA_SECRET)
60
+ .update(payload)
61
+ .digest("hex")
62
+ .toUpperCase()
63
+ .slice(0, 4);
64
+ if (checksum !== expected)
65
+ return null;
66
+ return { plan };
67
+ }
44
68
  function saveLicense(license) {
45
69
  if (!fs.existsSync(LICENSE_DIR)) {
46
70
  fs.mkdirSync(LICENSE_DIR, { recursive: true });
@@ -58,10 +82,6 @@ function readLicense() {
58
82
  }
59
83
  async function activate(key) {
60
84
  console.log("\n ⏳ Validando licença...");
61
- // -----------------------------------------------------------------
62
- // STUB: Enquanto o license server não existe, simula validação local
63
- // Aceita qualquer chave no formato LK-XXXX-XXXX-XXXX
64
- // -----------------------------------------------------------------
65
85
  const keyPattern = /^LK-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/i;
66
86
  if (!keyPattern.test(key)) {
67
87
  console.log(` ✗ Formato de chave inválido.`);
@@ -69,25 +89,27 @@ async function activate(key) {
69
89
  console.log(` Adquira em: https://lkpabap.ai\n`);
70
90
  process.exit(1);
71
91
  }
72
- // Simular chamada ao license server
73
- // TODO: Substituir por fetch real ao LICENSE_API_URL
74
- const isAllInOne = key.toUpperCase().startsWith("LK-A");
75
- const plan = key.toUpperCase().includes("ENT") ? "enterprise" : key.toUpperCase().includes("STA") ? "starter" : "pro";
92
+ // ── Tenta validação offline (beta keys com HMAC) ──────────────────
93
+ // TODO: Quando o license server existir, tentar fetch a api.lkpabap.ai primeiro
94
+ const beta = validateBetaKey(key);
95
+ if (!beta) {
96
+ console.log(` ✗ Chave inválida ou expirada.`);
97
+ console.log(` Verifique a chave recebida ou solicite uma nova.\n`);
98
+ process.exit(1);
99
+ }
100
+ const { plan } = beta;
76
101
  const license = {
77
102
  key: key.toUpperCase(),
78
103
  plan,
79
- mode: isAllInOne ? "allinone" : "byok",
80
104
  limits: {
81
105
  systems: plan === "starter" ? 1 : plan === "pro" ? 3 : -1,
82
106
  calls_per_day: plan === "starter" ? 200 : -1,
83
- tokens_per_month: isAllInOne ? (plan === "starter" ? 500000 : plan === "pro" ? 5000000 : 20000000) : undefined,
84
107
  },
85
- expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
108
+ expires: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
86
109
  validated_at: new Date().toISOString(),
87
- proxy_url: isAllInOne ? "https://proxy.lkpabap.ai/v1" : null,
88
110
  };
89
111
  saveLicense(license);
90
- const planLabel = `${plan.charAt(0).toUpperCase() + plan.slice(1)} (${license.mode === "byok" ? "BYOK" : "All-in-One"})`;
112
+ const planLabel = `${plan.charAt(0).toUpperCase() + plan.slice(1)} (BYOK)`;
91
113
  const systemsLabel = license.limits.systems === -1 ? "Ilimitado" : `até ${license.limits.systems}`;
92
114
  const callsLabel = license.limits.calls_per_day === -1 ? "Ilimitado" : `${license.limits.calls_per_day}/dia`;
93
115
  console.log(` ✓ Licença válida!`);
@@ -96,18 +118,9 @@ async function activate(key) {
96
118
  │ Plano: ${planLabel.padEnd(25)}│
97
119
  │ Válido até: ${license.expires.padEnd(25)}│
98
120
  │ Sistemas: ${systemsLabel.padEnd(25)}│
99
- │ Chamadas/dia: ${callsLabel.padEnd(25)}│`);
100
- if (license.mode === "allinone") {
101
- const tokensLabel = license.limits.tokens_per_month
102
- ? `${(license.limits.tokens_per_month / 1000000).toFixed(0)}M`
103
- : "N/A";
104
- console.log(` │ Tokens/mês: ${tokensLabel.padEnd(25)}│`);
105
- console.log(` │${" ".repeat(42)}│`);
106
- console.log(` │ ✓ Claude proxy configurado${" ".repeat(13)}│`);
107
- console.log(` │ URL: ${(license.proxy_url || "").padEnd(32)}│`);
108
- }
109
- console.log(` │${" ".repeat(42)}│`);
110
- console.log(` │ Licença salva em ~/.abap-ai/license${" ".repeat(4)}│`);
111
- console.log(` ╰──────────────────────────────────────────╯`);
121
+ │ Chamadas/dia: ${callsLabel.padEnd(25)}
122
+ │${" ".repeat(42)}│
123
+ │ Licença salva em ~/.abap-ai/license${" ".repeat(4)}│
124
+ ╰──────────────────────────────────────────╯`);
112
125
  console.log(`\n Próximo passo: abap-ai init\n`);
113
126
  }
package/dist/cli/init.js CHANGED
@@ -149,6 +149,7 @@ function addSystemToConfig(config, system) {
149
149
  SAP_USER: system.user,
150
150
  SAP_PASS: system.pass,
151
151
  SAP_LANGUAGE: system.language,
152
+ ABAP_AI_ENV_ROLE: system.environmentRole,
152
153
  };
153
154
  if (system.selfSignedSsl) {
154
155
  env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -214,6 +215,17 @@ async function promptSystem() {
214
215
  message: "Certificado SSL self-signed?",
215
216
  initial: true,
216
217
  },
218
+ {
219
+ type: "select",
220
+ name: "environmentRole",
221
+ message: "Papel do ambiente (controla quais operações o LKPABAP.ai pode executar)",
222
+ choices: [
223
+ { title: "DEVELOPMENT — leitura + escrita + criação (padrão para DEV)", value: "DEVELOPMENT" },
224
+ { title: "QUALITY — somente leitura (padrão para QAS/QA)", value: "QUALITY" },
225
+ { title: "PRODUCTION — somente leitura, mais restritivo (padrão para PRD)", value: "PRODUCTION" },
226
+ ],
227
+ initial: 0,
228
+ },
217
229
  ], {
218
230
  onCancel: () => {
219
231
  console.log("\n Cancelado.");
@@ -231,6 +243,7 @@ async function promptSystem() {
231
243
  pass: response.pass,
232
244
  language: (response.language?.trim() || "PT").toUpperCase(),
233
245
  selfSignedSsl: response.selfSignedSsl ?? true,
246
+ environmentRole: response.environmentRole || "DEVELOPMENT",
234
247
  };
235
248
  }
236
249
  // ---------------------------------------------------------------------------
@@ -239,7 +252,7 @@ async function promptSystem() {
239
252
  async function init() {
240
253
  console.log(`
241
254
  ╭─────────────────────────────────────╮
242
- │ LKPABAP.ia — Setup │
255
+ │ LKPABAP.ai — Setup │
243
256
  │ Conecte o Claude ao seu SAP │
244
257
  ╰─────────────────────────────────────╯
245
258
  `);
@@ -318,7 +331,9 @@ async function init() {
318
331
  const sn = `abap-${name}`;
319
332
  const env = config.mcpServers[sn]?.env || {};
320
333
  const client = env.SAP_CLIENT || "?";
321
- const line = ` │ • ${name} (client ${client})`;
334
+ const role = env.ABAP_AI_ENV_ROLE || "DEV";
335
+ const roleTag = role === "PRODUCTION" ? "PRD" : role === "QUALITY" ? "QAS" : "DEV";
336
+ const line = ` │ • ${name} (client ${client}, ${roleTag})`;
322
337
  console.log(`${line}${" ".repeat(Math.max(1, 48 - line.length))}│`);
323
338
  }
324
339
  console.log(` │${" ".repeat(46)}│
@@ -146,7 +146,7 @@ function countKnowledge() {
146
146
  async function status() {
147
147
  console.log(`
148
148
  ╭──────────────────────────────────────────────╮
149
- │ LKPABAP.ia v${VERSION}${" ".repeat(30 - VERSION.length)}│
149
+ │ LKPABAP.ai v${VERSION}${" ".repeat(30 - VERSION.length)}│
150
150
  ╰──────────────────────────────────────────────╯`);
151
151
  // Licença
152
152
  console.log(`
@@ -158,7 +158,7 @@ async function status() {
158
158
  const expires = new Date(license.expires);
159
159
  const daysLeft = Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
160
160
  const expired = daysLeft <= 0;
161
- const planLabel = `${license.plan.charAt(0).toUpperCase() + license.plan.slice(1)} (${license.mode === "byok" ? "BYOK" : "All-in-One"})`;
161
+ const planLabel = `${license.plan.charAt(0).toUpperCase() + license.plan.slice(1)} (BYOK)`;
162
162
  console.log(` Plano: ${planLabel}`);
163
163
  console.log(` Válido até: ${license.expires} (${expired ? "EXPIRADA" : `${daysLeft} dias restantes`})`);
164
164
  console.log(` Status: ${expired ? "✗ Expirada — renove em https://lkpabap.ai" : "✓ Ativa"}`);
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ const remove_js_1 = require("./cli/remove.js");
8
8
  const systems_js_1 = require("./cli/systems.js");
9
9
  const VERSION = "2.0.0";
10
10
  const HELP = `
11
- LKPABAP.ia — AI-powered ABAP development for SAP
11
+ LKPABAP.ai — AI-powered ABAP development for SAP
12
12
 
13
13
  Uso:
14
14
  abap-ai <comando> [opções]
@@ -38,7 +38,7 @@ async function main() {
38
38
  return;
39
39
  }
40
40
  if (command === "--version" || command === "-v") {
41
- console.log(`LKPABAP.ia v${VERSION}`);
41
+ console.log(`LKPABAP.ai v${VERSION}`);
42
42
  return;
43
43
  }
44
44
  switch (command) {
package/dist/index.js CHANGED
@@ -55,15 +55,64 @@ const knowledge_js_1 = require("./tools/knowledge.js");
55
55
  const create_dcl_js_1 = require("./tools/create-dcl.js");
56
56
  const create_amdp_js_1 = require("./tools/create-amdp.js");
57
57
  const system_profile_js_1 = require("./system-profile.js");
58
+ const security_policy_js_1 = require("./security-policy.js");
59
+ const security_audit_js_1 = require("./security-audit.js");
60
+ const license_guard_js_1 = require("./license-guard.js");
58
61
  const server = new mcp_js_1.McpServer({
59
62
  name: "abap-adt",
60
63
  version: "2.0.0",
61
64
  });
65
+ // ─── License Guard (wraps all tool handlers) ─────────────────────
66
+ // Intercepta server.tool para injetar licenseGuard() automaticamente.
67
+ // Toda tool retorna erro amigável se a licença é ausente ou expirada.
68
+ {
69
+ const _origTool = server.tool.bind(server);
70
+ server.tool = (...args) => {
71
+ const handler = args[args.length - 1];
72
+ args[args.length - 1] = async (...handlerArgs) => {
73
+ const licBlock = (0, license_guard_js_1.licenseGuard)();
74
+ if (licBlock)
75
+ return licBlock;
76
+ return await handler(...handlerArgs);
77
+ };
78
+ return _origTool.apply(server, args);
79
+ };
80
+ }
81
+ // ─── Security Guard ───────────────────────────────────────────────
82
+ /**
83
+ * Verifica se uma tool pode ser executada sob a security policy atual.
84
+ * Retorna null se permitido, ou uma resposta de erro MCP se bloqueado.
85
+ */
86
+ function securityGuard(toolName, transportRequest) {
87
+ const policy = (0, security_policy_js_1.getPolicy)();
88
+ const riskLevel = (0, security_policy_js_1.getToolRisk)(toolName);
89
+ // 1. Verificar acesso pela policy
90
+ const accessCheck = (0, security_policy_js_1.checkToolAccess)(toolName, policy);
91
+ if (!accessCheck.allowed) {
92
+ (0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, accessCheck.reason || "policy");
93
+ return {
94
+ content: [{ type: "text", text: accessCheck.reason || `Tool ${toolName} bloqueada.` }],
95
+ isError: true,
96
+ };
97
+ }
98
+ // 2. Verificar transport request obrigatório
99
+ const transportCheck = (0, security_policy_js_1.checkTransportRequired)(toolName, transportRequest, policy);
100
+ if (!transportCheck.allowed) {
101
+ (0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, transportCheck.reason || "transport required");
102
+ return {
103
+ content: [{ type: "text", text: transportCheck.reason || `Transport request obrigatório.` }],
104
+ isError: true,
105
+ };
106
+ }
107
+ // 3. Permitido — registrar no audit
108
+ (0, security_audit_js_1.auditAllowed)(toolName, riskLevel, policy.environment_role);
109
+ return null;
110
+ }
62
111
  // Tool: abap_read
63
112
  server.tool("abap_read", "Lê o código-fonte de um objeto ABAP no sistema SAP via ADT API. Para tabelas (TABL/DT), estruturas (TABL/DS) e table types (TTYP/TT), retorna a lista de campos com tipo e descrição.", {
64
113
  object_type: zod_1.z
65
- .enum(["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "TTYP/TT", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB"])
66
- .describe("Tipo do objeto SAP. Ex: PROG/P (report), CLAS/OC (classe), DDLS/DF (CDS view), TABL/DT (tabela), TABL/DS (estrutura), TTYP/TT (table type)."),
114
+ .enum(["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "TTYP/TT", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB", "MSAG/N", "ENHO/EO"])
115
+ .describe("Tipo do objeto SAP. Ex: PROG/P (report), CLAS/OC (classe), DDLS/DF (CDS view), TABL/DT (tabela), MSAG/N (classe de mensagens), ENHO/EO (enhancement implementation)."),
67
116
  object_name: zod_1.z
68
117
  .string()
69
118
  .describe("Nome do objeto ABAP (ex: ZR_SD_PEDIDOS_ABERTOS)."),
@@ -106,6 +155,9 @@ server.tool("abap_write", "Grava código-fonte em um objeto ABAP existente no si
106
155
  .optional()
107
156
  .describe("Ordem de transporte (ex: ECDK900123). Obrigatória para objetos em pacotes não-locais."),
108
157
  }, async ({ object_type, object_name, source, class_include, transport_request }) => {
158
+ const blocked = securityGuard("abap_write", transport_request);
159
+ if (blocked)
160
+ return blocked;
109
161
  try {
110
162
  const result = await (0, write_js_1.abapWrite)({ object_type, object_name, source, class_include, transport_request });
111
163
  return {
@@ -129,6 +181,9 @@ server.tool("abap_activate", "Ativa um objeto ABAP no sistema SAP via ADT API. R
129
181
  .string()
130
182
  .describe("Nome do objeto ABAP (ex: ZTESTEDENIS)."),
131
183
  }, async ({ object_type, object_name }) => {
184
+ const blocked = securityGuard("abap_activate");
185
+ if (blocked)
186
+ return blocked;
132
187
  try {
133
188
  const result = await (0, activate_js_1.abapActivate)({ object_type, object_name });
134
189
  return {
@@ -173,6 +228,9 @@ server.tool("abap_create", "Cria um novo objeto ABAP no sistema SAP via ADT API.
173
228
  .optional()
174
229
  .describe("Ordem de transporte (ex: ECDK900123). Se omitida, o SAP usa $TMP ou atribui automaticamente."),
175
230
  }, async ({ object_type, object_name, description, package: pkg, srvd_name, binding_type, transport_request }) => {
231
+ const blocked = securityGuard("abap_create", transport_request);
232
+ if (blocked)
233
+ return blocked;
176
234
  try {
177
235
  const result = await (0, create_js_1.abapCreate)({ object_type, object_name, description, package: pkg, srvd_name, binding_type, transport_request });
178
236
  return {
@@ -302,6 +360,9 @@ server.tool("abap_assign_transport", "Vincula um objeto ABAP existente a uma ord
302
360
  .string()
303
361
  .describe("Nome do objeto ABAP a vincular."),
304
362
  }, async ({ transport_request, object_type, object_name }) => {
363
+ const blocked = securityGuard("abap_assign_transport", transport_request);
364
+ if (blocked)
365
+ return blocked;
305
366
  try {
306
367
  const result = await (0, transports_js_1.abapAssignTransport)({ transport_request, object_type, object_name });
307
368
  return {
@@ -358,6 +419,9 @@ server.tool("abap_scaffold_rap", "Cria um stack RAP completo no SAP com 4 scenar
358
419
  .optional()
359
420
  .describe("Ordem de transporte para todos os objetos criados."),
360
421
  }, async ({ base_name, description, scenario, child_entities, with_feature_control, with_business_events, package: pkg, binding_type, transport_request }) => {
422
+ const blocked = securityGuard("abap_scaffold_rap", transport_request);
423
+ if (blocked)
424
+ return blocked;
361
425
  try {
362
426
  const result = await (0, scaffold_rap_js_1.abapScaffoldRap)({ base_name, description, scenario, child_entities, with_feature_control, with_business_events, package: pkg, binding_type, transport_request });
363
427
  return {
@@ -382,6 +446,9 @@ server.tool("abap_publish_binding", "Publica uma Service Binding (SRVB/SVB) no S
382
446
  .optional()
383
447
  .describe("Versão do serviço no Gateway (padrão: '0001')."),
384
448
  }, async ({ binding_name, service_version }) => {
449
+ const blocked = securityGuard("abap_publish_binding");
450
+ if (blocked)
451
+ return blocked;
385
452
  try {
386
453
  const result = await (0, publish_binding_js_1.abapPublishBinding)({ binding_name, service_version });
387
454
  return {
@@ -417,6 +484,9 @@ server.tool("abap_deploy_bsp", "Faz deploy de uma aplicação Fiori/UI5 para o A
417
484
  .optional()
418
485
  .describe("Ordem de transporte (ex: ECDK900123). Obrigatória se o pacote não for $TMP."),
419
486
  }, async ({ bsp_name, folder_path, package: pkg, description, transport_request }) => {
487
+ const blocked = securityGuard("abap_deploy_bsp", transport_request);
488
+ if (blocked)
489
+ return blocked;
420
490
  try {
421
491
  const result = await (0, deploy_bsp_js_1.abapDeployBsp)({ bsp_name, folder_path, package: pkg, description, transport_request });
422
492
  return {
@@ -492,8 +562,9 @@ server.tool("abap_data_preview", "Visualiza dados de uma tabela transparente ou
492
562
  .optional()
493
563
  .describe("Colunas a retornar, separadas por vírgula (ex: MATNR,MTART,MBRSH)."),
494
564
  }, async ({ object_name, max_rows, where_clause, order_by, columns }) => {
565
+ const effectiveRows = (0, security_policy_js_1.getEffectiveMaxRows)(max_rows ?? 100, (0, security_policy_js_1.getPolicy)());
495
566
  try {
496
- const result = await (0, data_preview_js_1.abapDataPreview)({ object_name, max_rows, where_clause, order_by, columns });
567
+ const result = await (0, data_preview_js_1.abapDataPreview)({ object_name, max_rows: effectiveRows, where_clause, order_by, columns });
497
568
  return {
498
569
  content: [{ type: "text", text: result }],
499
570
  };
@@ -721,6 +792,9 @@ server.tool("abap_release_transport", "Libera uma ordem de transporte no SAP. AT
721
792
  .string()
722
793
  .describe("Número da ordem de transporte a liberar (ex: ECDK900123)."),
723
794
  }, async ({ transport_request }) => {
795
+ const blocked = securityGuard("abap_release_transport", transport_request);
796
+ if (blocked)
797
+ return blocked;
724
798
  try {
725
799
  const result = await (0, release_transport_js_1.abapReleaseTransport)({ transport_request });
726
800
  return {
@@ -748,6 +822,9 @@ server.tool("abap_delete", "Exclui um objeto ABAP do sistema SAP. ATENÇÃO: ope
748
822
  .optional()
749
823
  .describe("Ordem de transporte (obrigatória para objetos em pacotes não-locais)."),
750
824
  }, async ({ object_type, object_name, transport_request }) => {
825
+ const blocked = securityGuard("abap_delete", transport_request);
826
+ if (blocked)
827
+ return blocked;
751
828
  try {
752
829
  const result = await (0, delete_js_1.abapDeleteObject)({ object_type, object_name, transport_request });
753
830
  return {
@@ -787,6 +864,9 @@ server.tool("abap_refactor_rename", "Renomeia um símbolo (variável, método, a
787
864
  .optional()
788
865
  .describe("Include da classe. Apenas para CLAS/OC. Padrão: main."),
789
866
  }, async ({ object_type, object_name, old_name, new_name, line, column, class_include }) => {
867
+ const blocked = securityGuard("abap_refactor_rename");
868
+ if (blocked)
869
+ return blocked;
790
870
  try {
791
871
  const result = await (0, refactor_rename_js_1.abapRefactorRename)({ object_type, object_name, old_name, new_name, line, column, class_include });
792
872
  return {
@@ -879,6 +959,9 @@ server.tool("abap_quick_fix", "Obtém e aplica correções rápidas (quick fixes
879
959
  .describe("Include da classe. Apenas para CLAS/OC."),
880
960
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte para aplicar o fix."),
881
961
  }, async ({ object_type, object_name, line, column, fix_index, class_include, transport_request }) => {
962
+ const blocked = securityGuard("abap_quick_fix", transport_request);
963
+ if (blocked)
964
+ return blocked;
882
965
  try {
883
966
  const result = await (0, quick_fix_js_1.abapQuickFix)({ object_type, object_name, line, column, fix_index, class_include, transport_request });
884
967
  return { content: [{ type: "text", text: result }] };
@@ -903,6 +986,9 @@ server.tool("abap_extract_method", "Extrai um trecho de código ABAP selecionado
903
986
  .describe("Include da classe. Apenas para CLAS/OC."),
904
987
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte (se necessário)."),
905
988
  }, async ({ object_type, object_name, start_line, end_line, new_method_name, class_include, transport_request }) => {
989
+ const blocked = securityGuard("abap_extract_method", transport_request);
990
+ if (blocked)
991
+ return blocked;
906
992
  try {
907
993
  const result = await (0, extract_method_js_1.abapExtractMethod)({ object_type, object_name, start_line, end_line, new_method_name, class_include, transport_request });
908
994
  return { content: [{ type: "text", text: result }] };
@@ -933,8 +1019,9 @@ server.tool("abap_sql_console", "Executa uma consulta ABAP SQL (Open SQL) no sis
933
1019
  sql: zod_1.z.string().describe("Consulta ABAP SQL. Ex: SELECT * FROM mara WHERE matnr LIKE 'Z%' ORDER BY matnr UP TO 100 ROWS"),
934
1020
  max_rows: zod_1.z.number().optional().describe("Número máximo de linhas retornadas (padrão: 100)."),
935
1021
  }, async ({ sql, max_rows }) => {
1022
+ const effectiveRows = (0, security_policy_js_1.getEffectiveMaxRows)(max_rows ?? 100, (0, security_policy_js_1.getPolicy)());
936
1023
  try {
937
- const result = await (0, sql_console_js_1.abapSqlConsole)({ sql, max_rows });
1024
+ const result = await (0, sql_console_js_1.abapSqlConsole)({ sql, max_rows: effectiveRows });
938
1025
  return { content: [{ type: "text", text: result }] };
939
1026
  }
940
1027
  catch (error) {
@@ -961,6 +1048,9 @@ server.tool("abap_create_transport", "Cria uma nova ordem de transporte (workben
961
1048
  type: zod_1.z.enum(["W", "K"]).optional().describe("Tipo: W=Workbench (padrão), K=Customizing."),
962
1049
  target: zod_1.z.string().optional().describe("Sistema de destino (ex: QAS). Opcional."),
963
1050
  }, async ({ description, type, target }) => {
1051
+ const blocked = securityGuard("abap_create_transport");
1052
+ if (blocked)
1053
+ return blocked;
964
1054
  try {
965
1055
  const result = await (0, create_transport_js_1.abapCreateTransport)({ description, type, target });
966
1056
  return { content: [{ type: "text", text: result }] };
@@ -1144,6 +1234,9 @@ server.tool("abap_git_pull", "Executa pull de um repositório abapGit no sistema
1144
1234
  branch: zod_1.z.string().optional().describe("Nome do branch (padrão: main)."),
1145
1235
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte para os objetos importados."),
1146
1236
  }, async ({ repo_id, branch, transport_request }) => {
1237
+ const blocked = securityGuard("abapgit_pull", transport_request);
1238
+ if (blocked)
1239
+ return blocked;
1147
1240
  try {
1148
1241
  const result = await (0, abapgit_js_1.abapGitPull)({ repo_id, branch, transport_request });
1149
1242
  return { content: [{ type: "text", text: result }] };
@@ -1163,6 +1256,9 @@ server.tool("abap_git_stage", "Mostra status de staging e executa push para um r
1163
1256
  message: zod_1.z.string().optional().describe("Mensagem de commit (obrigatória para push)."),
1164
1257
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
1165
1258
  }, async ({ repo_id, action, message: commitMsg, transport_request }) => {
1259
+ const blocked = securityGuard("abapgit_stage", transport_request);
1260
+ if (blocked)
1261
+ return blocked;
1166
1262
  try {
1167
1263
  const result = await (0, abapgit_js_1.abapGitStage)({ repo_id, action, message: commitMsg, transport_request });
1168
1264
  return { content: [{ type: "text", text: result }] };
@@ -1233,6 +1329,9 @@ server.tool("abap_create_dcl", "Cria um objeto DCL (Data Control Language) para
1233
1329
  package: zod_1.z.string().optional().describe("Pacote SAP. Padrão: '$TMP'."),
1234
1330
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
1235
1331
  }, async ({ dcl_name, cds_entity, auth_type, auth_mappings, literal_conditions, with_user_aspect, package: pkg, transport_request }) => {
1332
+ const blocked = securityGuard("abap_create_dcl", transport_request);
1333
+ if (blocked)
1334
+ return blocked;
1236
1335
  try {
1237
1336
  const result = await (0, create_dcl_js_1.abapCreateDcl)({ dcl_name, cds_entity, auth_type, auth_mappings, literal_conditions, with_user_aspect, package: pkg, transport_request });
1238
1337
  return { content: [{ type: "text", text: result }] };
@@ -1262,6 +1361,9 @@ server.tool("abap_create_amdp", "Cria uma classe AMDP (ABAP Managed Database Pro
1262
1361
  package: zod_1.z.string().optional().describe("Pacote SAP. Padrão: '$TMP'."),
1263
1362
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
1264
1363
  }, async ({ class_name, method_name, method_type, importing: imp, exporting_table, cds_entity, description, package: pkg, transport_request }) => {
1364
+ const blocked = securityGuard("abap_create_amdp", transport_request);
1365
+ if (blocked)
1366
+ return blocked;
1265
1367
  try {
1266
1368
  const result = await (0, create_amdp_js_1.abapCreateAmdp)({ class_name, method_name, method_type, importing: imp, exporting_table, cds_entity, description, package: pkg, transport_request });
1267
1369
  return { content: [{ type: "text", text: result }] };
@@ -1305,12 +1407,32 @@ server.resource("system-profile", "abap://system-profile", { description: "Perfi
1305
1407
  }],
1306
1408
  };
1307
1409
  });
1410
+ // Resource: security-policy
1411
+ server.resource("security-policy", "abap://security-policy", { description: "Política de segurança ativa — environment role, níveis permitidos, tools bloqueadas, limites. Configurável via env ABAP_AI_ENV_ROLE ou arquivo ~/.abap-ai/security-policy.json." }, async () => {
1412
+ const policy = (0, security_policy_js_1.getPolicy)();
1413
+ const toolRiskMap = {
1414
+ blocked_tools: policy.always_blocked,
1415
+ environment: policy.environment_role,
1416
+ allowed_operations: policy.allowed_levels,
1417
+ require_transport: policy.require_transport,
1418
+ max_preview_rows: policy.max_preview_rows,
1419
+ };
1420
+ return {
1421
+ contents: [{
1422
+ uri: "abap://security-policy",
1423
+ mimeType: "application/json",
1424
+ text: JSON.stringify(toolRiskMap, null, 2),
1425
+ }],
1426
+ };
1427
+ });
1308
1428
  // Inicializa o servidor via stdio (modo Claude Code MCP)
1309
1429
  async function main() {
1310
1430
  const transport = new stdio_js_1.StdioServerTransport();
1311
1431
  await server.connect(transport);
1312
1432
  const profile = (0, system_profile_js_1.getProfile)();
1313
- process.stderr.write(`abap-mcp-server v2.0.0 iniciado | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform}\n`);
1433
+ const policy = (0, security_policy_js_1.getPolicy)();
1434
+ const licLabel = (0, license_guard_js_1.licenseStatusLabel)();
1435
+ process.stderr.write(`abap-mcp-server v2.2.0 iniciado | License: ${licLabel} | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")})\n`);
1314
1436
  }
1315
1437
  main().catch((err) => {
1316
1438
  process.stderr.write(`Falha ao iniciar servidor: ${err}\n`);
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * License Guard — valida licença antes de executar qualquer tool.
4
+ *
5
+ * Singleton: lê ~/.abap-ai/license.json uma vez e cacheia.
6
+ * licenseGuard() retorna null se OK, ou MCP error response se bloqueado.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.getLicense = getLicense;
10
+ exports.reloadLicense = reloadLicense;
11
+ exports.licenseGuard = licenseGuard;
12
+ exports.licenseStatusLabel = licenseStatusLabel;
13
+ const activate_js_1 = require("./cli/activate.js");
14
+ // ─── Singleton ───────────────────────────────────────────────────
15
+ let cachedLicense = undefined;
16
+ /**
17
+ * Retorna a licença cacheada. null = arquivo não existe ou inválido.
18
+ */
19
+ function getLicense() {
20
+ if (cachedLicense === undefined) {
21
+ cachedLicense = (0, activate_js_1.readLicense)();
22
+ }
23
+ return cachedLicense;
24
+ }
25
+ /**
26
+ * Força releitura do license.json (útil após activate).
27
+ */
28
+ function reloadLicense() {
29
+ cachedLicense = undefined;
30
+ return getLicense();
31
+ }
32
+ function daysUntilExpiry(expiresStr) {
33
+ const expires = new Date(expiresStr + "T23:59:59");
34
+ const now = new Date();
35
+ return Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
36
+ }
37
+ /**
38
+ * Verifica se a licença é válida.
39
+ * Retorna null se OK, ou MCP error response se bloqueado.
40
+ */
41
+ function licenseGuard() {
42
+ const license = getLicense();
43
+ if (!license) {
44
+ return {
45
+ content: [{
46
+ type: "text",
47
+ text: "LKPABAP.ai — Licença não encontrada.\n\n" +
48
+ "Para usar as ferramentas, ative sua licença:\n\n" +
49
+ " abap-ai activate LK-XXXX-XXXX-XXXX\n\n" +
50
+ "Adquira em: https://lkpabap.ai",
51
+ }],
52
+ isError: true,
53
+ };
54
+ }
55
+ const daysLeft = daysUntilExpiry(license.expires);
56
+ if (daysLeft <= 0) {
57
+ return {
58
+ content: [{
59
+ type: "text",
60
+ text: `LKPABAP.ai — Licença expirada em ${license.expires}.\n\n` +
61
+ "Renove sua licença:\n\n" +
62
+ " abap-ai activate <NOVA-CHAVE>\n\n" +
63
+ "Renove em: https://lkpabap.ai",
64
+ }],
65
+ isError: true,
66
+ };
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * Retorna string de status para log no startup.
72
+ */
73
+ function licenseStatusLabel() {
74
+ const license = getLicense();
75
+ if (!license)
76
+ return "SEM LICENÇA";
77
+ const daysLeft = daysUntilExpiry(license.expires);
78
+ if (daysLeft <= 0)
79
+ return "EXPIRADA";
80
+ return `${license.plan} até ${license.expires}`;
81
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * Security Audit Log — registro de operações de segurança.
4
+ *
5
+ * Registra todas as operações WRITE, CREATE, DESTRUCTIVE e ADMIN,
6
+ * incluindo tentativas bloqueadas pela security policy.
7
+ * Separado do logger de chamadas ADT — este é focado em governança.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.auditLog = auditLog;
11
+ exports.auditAllowed = auditAllowed;
12
+ exports.auditBlocked = auditBlocked;
13
+ exports.getSessionAudit = getSessionAudit;
14
+ exports.getAuditSummary = getAuditSummary;
15
+ exports.readTodayAudit = readTodayAudit;
16
+ const fs_1 = require("fs");
17
+ const path_1 = require("path");
18
+ // ─── Config ───────────────────────────────────────────────────────
19
+ const AUDIT_DIR = process.env.ABAP_AI_AUDIT_DIR || (0, path_1.join)(process.env.HOME || "~", ".abap-ai", "audit");
20
+ const AUDIT_ENABLED = process.env.ABAP_AI_AUDIT !== "false";
21
+ // ─── Estado da sessão ─────────────────────────────────────────────
22
+ const sessionAuditEntries = [];
23
+ // ─── Funções ──────────────────────────────────────────────────────
24
+ function ensureAuditDir() {
25
+ if (!(0, fs_1.existsSync)(AUDIT_DIR)) {
26
+ (0, fs_1.mkdirSync)(AUDIT_DIR, { recursive: true });
27
+ }
28
+ }
29
+ function auditFileName() {
30
+ const date = new Date().toISOString().slice(0, 10);
31
+ return (0, path_1.join)(AUDIT_DIR, `security-${date}.jsonl`);
32
+ }
33
+ /**
34
+ * Extrai o hostname do SAP_URL para identificar o sistema no audit.
35
+ */
36
+ function getSystemId() {
37
+ const url = process.env.SAP_URL || "unknown";
38
+ try {
39
+ return new URL(url).hostname;
40
+ }
41
+ catch {
42
+ return url;
43
+ }
44
+ }
45
+ /**
46
+ * Registra uma entrada no audit log de segurança.
47
+ */
48
+ function auditLog(entry) {
49
+ const fullEntry = {
50
+ timestamp: new Date().toISOString(),
51
+ system: getSystemId(),
52
+ user: process.env.SAP_USER || "unknown",
53
+ ...entry,
54
+ };
55
+ sessionAuditEntries.push(fullEntry);
56
+ if (!AUDIT_ENABLED)
57
+ return;
58
+ try {
59
+ ensureAuditDir();
60
+ (0, fs_1.appendFileSync)(auditFileName(), JSON.stringify(fullEntry) + "\n", "utf-8");
61
+ }
62
+ catch {
63
+ // Audit silencioso — não deve quebrar operações
64
+ }
65
+ }
66
+ /**
67
+ * Atalho para registrar uma operação permitida.
68
+ */
69
+ function auditAllowed(tool, riskLevel, environment, params) {
70
+ auditLog({
71
+ tool,
72
+ risk_level: riskLevel,
73
+ environment,
74
+ result: "ALLOWED",
75
+ params,
76
+ });
77
+ }
78
+ /**
79
+ * Atalho para registrar uma operação bloqueada.
80
+ */
81
+ function auditBlocked(tool, riskLevel, environment, reason, params) {
82
+ auditLog({
83
+ tool,
84
+ risk_level: riskLevel,
85
+ environment,
86
+ result: "BLOCKED",
87
+ reason,
88
+ params,
89
+ });
90
+ }
91
+ /**
92
+ * Retorna as entradas de audit da sessão atual.
93
+ */
94
+ function getSessionAudit() {
95
+ return [...sessionAuditEntries];
96
+ }
97
+ /**
98
+ * Retorna resumo do audit da sessão para exibição.
99
+ */
100
+ function getAuditSummary() {
101
+ const allowed = sessionAuditEntries.filter((e) => e.result === "ALLOWED").length;
102
+ const blocked = sessionAuditEntries.filter((e) => e.result === "BLOCKED").length;
103
+ const byTool = {};
104
+ for (const entry of sessionAuditEntries) {
105
+ if (!byTool[entry.tool])
106
+ byTool[entry.tool] = { allowed: 0, blocked: 0 };
107
+ if (entry.result === "ALLOWED")
108
+ byTool[entry.tool].allowed++;
109
+ else
110
+ byTool[entry.tool].blocked++;
111
+ }
112
+ return {
113
+ total: sessionAuditEntries.length,
114
+ allowed,
115
+ blocked,
116
+ by_tool: byTool,
117
+ };
118
+ }
119
+ /**
120
+ * Lê o audit log do dia atual.
121
+ */
122
+ function readTodayAudit() {
123
+ try {
124
+ const file = auditFileName();
125
+ if (!(0, fs_1.existsSync)(file))
126
+ return [];
127
+ const content = (0, fs_1.readFileSync)(file, "utf-8");
128
+ return content
129
+ .split("\n")
130
+ .filter((line) => line.trim())
131
+ .map((line) => JSON.parse(line));
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ }
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ /**
3
+ * Security Policy — controle de acesso por camada de ambiente SAP.
4
+ *
5
+ * Implementa o princípio: "O LKPABAP.ai NUNCA sobrepõe as restrições de
6
+ * segurança do SAP. Em PRD, só leitura. Em QAS, leitura + debug.
7
+ * Em DEV, tudo liberado. abap_release_transport sempre bloqueado."
8
+ *
9
+ * A autorização de dados (S_TADIR, S_DEVELOP, etc.) é 100% delegada ao SAP.
10
+ * Esta camada controla quais CATEGORIAS de operação o MCP server aceita executar.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.getToolRisk = getToolRisk;
14
+ exports.loadPolicy = loadPolicy;
15
+ exports.checkToolAccess = checkToolAccess;
16
+ exports.getEffectiveMaxRows = getEffectiveMaxRows;
17
+ exports.checkTransportRequired = checkTransportRequired;
18
+ exports.getPolicy = getPolicy;
19
+ exports.setPolicy = setPolicy;
20
+ exports.getDefaultPolicy = getDefaultPolicy;
21
+ exports.listEnvironmentRoles = listEnvironmentRoles;
22
+ const fs_1 = require("fs");
23
+ const path_1 = require("path");
24
+ /**
25
+ * Mapa de cada tool para seu nível de risco.
26
+ * Tools não listadas aqui são consideradas READ (safe by default).
27
+ */
28
+ const TOOL_RISK = {
29
+ // READ — sem guard extra (default para tools não listadas)
30
+ abap_read: "READ",
31
+ abap_search: "READ",
32
+ abap_where_used: "READ",
33
+ abap_check: "READ",
34
+ abap_object_structure: "READ",
35
+ abap_data_preview: "READ",
36
+ abap_sql_console: "READ",
37
+ abap_element_info: "READ",
38
+ abap_pretty_printer: "READ",
39
+ abap_unit_test: "READ",
40
+ abap_atc_check: "READ",
41
+ abap_package_contents: "READ",
42
+ abap_object_versions: "READ",
43
+ abap_function_group: "READ",
44
+ abap_message_class: "READ",
45
+ abap_doc: "READ",
46
+ abap_code_completion: "READ",
47
+ abap_navigate: "READ",
48
+ abap_call_hierarchy: "READ",
49
+ abap_code_coverage: "READ",
50
+ abap_transport_contents: "READ",
51
+ abap_cds_annotations: "READ",
52
+ abap_cds_dependencies: "READ",
53
+ abap_annotation_propagation: "READ",
54
+ abap_enhancement_spot: "READ",
55
+ abap_lock_object: "READ",
56
+ abap_auth_object: "READ",
57
+ abap_number_range: "READ",
58
+ abap_repository_tree: "READ",
59
+ abap_discovery: "READ",
60
+ abap_released_apis: "READ",
61
+ abap_object_documentation: "READ",
62
+ abap_system_info: "READ",
63
+ abap_list_transports: "READ",
64
+ abap_knowledge: "READ",
65
+ abap_traces: "READ",
66
+ abap_breakpoints: "READ",
67
+ abapgit_repos: "READ",
68
+ // WRITE — modifica objetos existentes
69
+ abap_write: "WRITE",
70
+ abap_activate: "WRITE",
71
+ abap_refactor_rename: "WRITE",
72
+ abap_extract_method: "WRITE",
73
+ abap_quick_fix: "WRITE",
74
+ abap_publish_binding: "WRITE",
75
+ abap_deploy_bsp: "WRITE",
76
+ abap_assign_transport: "WRITE",
77
+ abapgit_pull: "WRITE",
78
+ abapgit_stage: "WRITE",
79
+ // CREATE — cria objetos novos no SAP
80
+ abap_create: "CREATE",
81
+ abap_create_transport: "CREATE",
82
+ abap_create_dcl: "CREATE",
83
+ abap_create_amdp: "CREATE",
84
+ abap_scaffold_rap: "CREATE",
85
+ // WRITE — delete segue a mesma regra de escrita (permitido em DEV, bloqueado em QAS/PRD)
86
+ abap_delete: "WRITE",
87
+ // ADMIN — operações administrativas de alto impacto (sempre bloqueado)
88
+ abap_release_transport: "ADMIN",
89
+ };
90
+ /**
91
+ * Retorna o nível de risco de uma tool.
92
+ */
93
+ function getToolRisk(toolName) {
94
+ return TOOL_RISK[toolName] ?? "READ";
95
+ }
96
+ // ─── Policies padrão por ambiente ─────────────────────────────────
97
+ const POLICY_DEVELOPMENT = {
98
+ environment_role: "DEVELOPMENT",
99
+ allowed_levels: ["READ", "WRITE", "CREATE"],
100
+ always_blocked: ["abap_release_transport"],
101
+ require_transport: false,
102
+ max_preview_rows: 0,
103
+ };
104
+ const POLICY_QUALITY = {
105
+ environment_role: "QUALITY",
106
+ allowed_levels: ["READ"],
107
+ always_blocked: [
108
+ "abap_release_transport",
109
+ "abap_delete",
110
+ "abap_scaffold_rap",
111
+ "abap_create",
112
+ "abap_create_dcl",
113
+ "abap_create_amdp",
114
+ "abap_write",
115
+ "abap_activate",
116
+ "abap_refactor_rename",
117
+ "abap_extract_method",
118
+ "abap_quick_fix",
119
+ "abap_publish_binding",
120
+ "abap_deploy_bsp",
121
+ "abapgit_pull",
122
+ ],
123
+ require_transport: true,
124
+ max_preview_rows: 100,
125
+ };
126
+ const POLICY_PRODUCTION = {
127
+ environment_role: "PRODUCTION",
128
+ allowed_levels: ["READ"],
129
+ always_blocked: [
130
+ "abap_release_transport",
131
+ "abap_delete",
132
+ "abap_scaffold_rap",
133
+ "abap_create",
134
+ "abap_create_dcl",
135
+ "abap_create_amdp",
136
+ "abap_create_transport",
137
+ "abap_write",
138
+ "abap_activate",
139
+ "abap_refactor_rename",
140
+ "abap_extract_method",
141
+ "abap_quick_fix",
142
+ "abap_publish_binding",
143
+ "abap_deploy_bsp",
144
+ "abapgit_pull",
145
+ "abapgit_stage",
146
+ "abap_assign_transport",
147
+ ],
148
+ require_transport: true,
149
+ max_preview_rows: 50,
150
+ };
151
+ const DEFAULT_POLICIES = {
152
+ DEVELOPMENT: POLICY_DEVELOPMENT,
153
+ QUALITY: POLICY_QUALITY,
154
+ PRODUCTION: POLICY_PRODUCTION,
155
+ };
156
+ // ─── Carregamento da policy ───────────────────────────────────────
157
+ /**
158
+ * Caminho do arquivo de policy central (planos Enterprise/Corporate).
159
+ * Gestores podem travar a policy criando este arquivo.
160
+ */
161
+ const CENTRAL_POLICY_PATH = (0, path_1.join)(process.env.HOME || "~", ".abap-ai", "security-policy.json");
162
+ /**
163
+ * Carrega a policy do ambiente a partir de:
164
+ * 1. Variável de ambiente ABAP_AI_SECURITY_POLICY (JSON inline)
165
+ * 2. Arquivo central ~/.abap-ai/security-policy.json (Enterprise/Corporate)
166
+ * 3. Variável ABAP_AI_ENV_ROLE (DEVELOPMENT | QUALITY | PRODUCTION)
167
+ * 4. Default: DEVELOPMENT
168
+ */
169
+ function loadPolicy() {
170
+ // 1. JSON inline (env var)
171
+ const policyJson = process.env.ABAP_AI_SECURITY_POLICY;
172
+ if (policyJson) {
173
+ try {
174
+ const parsed = JSON.parse(policyJson);
175
+ return mergeWithDefault(parsed);
176
+ }
177
+ catch {
178
+ // fallthrough
179
+ }
180
+ }
181
+ // 2. Arquivo central (gestores Enterprise/Corporate)
182
+ if ((0, fs_1.existsSync)(CENTRAL_POLICY_PATH)) {
183
+ try {
184
+ const raw = (0, fs_1.readFileSync)(CENTRAL_POLICY_PATH, "utf-8");
185
+ const parsed = JSON.parse(raw);
186
+ return mergeWithDefault(parsed);
187
+ }
188
+ catch {
189
+ // fallthrough
190
+ }
191
+ }
192
+ // 3. Env role simples
193
+ const envRole = (process.env.ABAP_AI_ENV_ROLE || "").toUpperCase();
194
+ if (DEFAULT_POLICIES[envRole]) {
195
+ return deepClone(DEFAULT_POLICIES[envRole]);
196
+ }
197
+ // 4. Default: DEVELOPMENT
198
+ return deepClone(POLICY_DEVELOPMENT);
199
+ }
200
+ /**
201
+ * Verifica se uma tool pode ser executada sob a policy atual.
202
+ */
203
+ function checkToolAccess(toolName, policy) {
204
+ // 1. abap_release_transport — SEMPRE bloqueado, independente de qualquer config
205
+ if (toolName === "abap_release_transport") {
206
+ return {
207
+ allowed: false,
208
+ reason: `BLOQUEADO: abap_release_transport é sempre bloqueado pelo LKPABAP.ai. `
209
+ + `Liberar transportes é uma operação irreversível que pode propagar mudanças para ambientes produtivos. `
210
+ + `Execute esta operação manualmente na transação SE09/SE10 do SAP GUI.`,
211
+ };
212
+ }
213
+ // 2. Tool na lista de always_blocked
214
+ if (policy.always_blocked.includes(toolName)) {
215
+ return {
216
+ allowed: false,
217
+ reason: `BLOQUEADO: A tool "${toolName}" não é permitida no ambiente ${policy.environment_role}. `
218
+ + `Esta restrição é definida pela política de segurança do sistema. `
219
+ + describeEnvironmentRestriction(policy.environment_role),
220
+ };
221
+ }
222
+ // 3. Nível de risco da tool vs níveis permitidos
223
+ const riskLevel = getToolRisk(toolName);
224
+ if (!policy.allowed_levels.includes(riskLevel)) {
225
+ return {
226
+ allowed: false,
227
+ reason: `BLOQUEADO: A tool "${toolName}" requer nível "${riskLevel}", `
228
+ + `mas o ambiente ${policy.environment_role} só permite: ${policy.allowed_levels.join(", ")}. `
229
+ + describeEnvironmentRestriction(policy.environment_role),
230
+ };
231
+ }
232
+ return { allowed: true };
233
+ }
234
+ /**
235
+ * Retorna o limite de rows para preview/sql considerando a policy.
236
+ * Se a policy define um limite, usa o menor entre o limite da policy e o solicitado.
237
+ */
238
+ function getEffectiveMaxRows(requestedRows, policy) {
239
+ if (policy.max_preview_rows <= 0)
240
+ return requestedRows;
241
+ return Math.min(requestedRows, policy.max_preview_rows);
242
+ }
243
+ /**
244
+ * Verifica se transport request é obrigatório e não foi fornecido.
245
+ */
246
+ function checkTransportRequired(toolName, transportRequest, policy) {
247
+ const riskLevel = getToolRisk(toolName);
248
+ if (policy.require_transport
249
+ && (riskLevel === "WRITE" || riskLevel === "CREATE")
250
+ && !transportRequest) {
251
+ return {
252
+ allowed: false,
253
+ reason: `BLOQUEADO: O ambiente ${policy.environment_role} exige transport request para operações de ${riskLevel}. `
254
+ + `Informe o parâmetro transport_request (ex: "DEVK900123"). `
255
+ + `Use abap_list_transports ou abap_create_transport para obter um.`,
256
+ };
257
+ }
258
+ return { allowed: true };
259
+ }
260
+ // ─── Singleton ────────────────────────────────────────────────────
261
+ let currentPolicy = null;
262
+ /**
263
+ * Retorna a policy atual. Carrega do ambiente/arquivo na primeira chamada.
264
+ */
265
+ function getPolicy() {
266
+ if (!currentPolicy) {
267
+ currentPolicy = loadPolicy();
268
+ }
269
+ return currentPolicy;
270
+ }
271
+ /**
272
+ * Força uma policy específica (usado em testes ou override programático).
273
+ */
274
+ function setPolicy(policy) {
275
+ currentPolicy = policy;
276
+ }
277
+ /**
278
+ * Retorna a policy padrão para um environment role.
279
+ */
280
+ function getDefaultPolicy(role) {
281
+ return deepClone(DEFAULT_POLICIES[role] || POLICY_DEVELOPMENT);
282
+ }
283
+ /**
284
+ * Lista os environment roles disponíveis.
285
+ */
286
+ function listEnvironmentRoles() {
287
+ return ["DEVELOPMENT", "QUALITY", "PRODUCTION"];
288
+ }
289
+ // ─── Helpers ──────────────────────────────────────────────────────
290
+ function describeEnvironmentRestriction(role) {
291
+ switch (role) {
292
+ case "PRODUCTION":
293
+ return "Em ambientes PRODUTIVOS, o LKPABAP.ai opera somente em modo leitura. "
294
+ + "Qualquer modificação deve ser feita via transporte a partir do ambiente de desenvolvimento.";
295
+ case "QUALITY":
296
+ return "Em ambientes de QUALIDADE (QAS), o LKPABAP.ai opera somente em modo leitura. "
297
+ + "Modificações devem ser transportadas a partir do ambiente de desenvolvimento.";
298
+ case "DEVELOPMENT":
299
+ return ""; // DEV não tem restrição descritiva
300
+ default:
301
+ return "";
302
+ }
303
+ }
304
+ function mergeWithDefault(partial) {
305
+ const role = partial.environment_role || "DEVELOPMENT";
306
+ const base = deepClone(DEFAULT_POLICIES[role] || POLICY_DEVELOPMENT);
307
+ return {
308
+ environment_role: partial.environment_role ?? base.environment_role,
309
+ allowed_levels: partial.allowed_levels ?? base.allowed_levels,
310
+ always_blocked: [
311
+ ...new Set([
312
+ ...base.always_blocked,
313
+ ...(partial.always_blocked ?? []),
314
+ ]),
315
+ ],
316
+ require_transport: partial.require_transport ?? base.require_transport,
317
+ max_preview_rows: partial.max_preview_rows ?? base.max_preview_rows,
318
+ };
319
+ }
320
+ function deepClone(obj) {
321
+ return JSON.parse(JSON.stringify(obj));
322
+ }
@@ -32,6 +32,9 @@ async function abapActivate(input) {
32
32
  },
33
33
  params: { method: "activate", preauditRequested: "true" },
34
34
  responseType: "text",
35
+ // Aceita 200 (ativação OK) e 400 (erros de sintaxe) — sem isso, axios rejeita non-2xx
36
+ // e o erro pode não ser propagado corretamente pelo MCP SDK
37
+ validateStatus: (status) => status === 200 || status === 400,
35
38
  });
36
39
  const { success, alreadyActive, messages } = parseActivationResponse(response.data);
37
40
  if (!success) {
@@ -4,6 +4,7 @@ exports.abapReadTool = void 0;
4
4
  exports.abapRead = abapRead;
5
5
  const adt_client_js_1 = require("../adt-client.js");
6
6
  const object_versions_js_1 = require("./object-versions.js");
7
+ const message_class_js_1 = require("./message-class.js");
7
8
  function resolveSourcePath(adtPath, name, objectType, classInclude) {
8
9
  if (objectType === "CLAS/OC" && classInclude && classInclude !== "main") {
9
10
  return `/${adtPath}/${name}/includes/${classInclude}/source/main`;
@@ -106,6 +107,10 @@ async function abapRead(input) {
106
107
  const { object_type, object_name, class_include } = input;
107
108
  const name = object_name.toUpperCase();
108
109
  const adtPath = (0, adt_client_js_1.resolveAdtPath)(object_type);
110
+ // MSAG/N: delegar para abapMessageClass que já faz parsing do XML
111
+ if (object_type === "MSAG/N") {
112
+ return (0, message_class_js_1.abapMessageClass)({ message_class_name: name });
113
+ }
109
114
  // Para TABL/DT, TABL/DS e TTYP/TT: tentar /source/main primeiro (CDS DDL), se 404 ler metadados XML
110
115
  if (DDIC_METADATA_TYPES.includes(object_type)) {
111
116
  const nameLower = name.toLowerCase();
@@ -155,7 +160,7 @@ exports.abapReadTool = {
155
160
  object_type: {
156
161
  type: "string",
157
162
  description: "Tipo do objeto SAP. Exemplos: PROG/P (report), CLAS/OC (classe), FUGR/FF (function module), DDLS/DF (CDS view), TABL/DT (tabela transparente), TABL/DS (estrutura), INTF/OI (interface).",
158
- enum: ["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB"],
163
+ enum: ["PROG/P", "CLAS/OC", "FUGR/FF", "DOMA/D", "DTEL/D", "TABL/DT", "TABL/DS", "TTYP/TT", "INTF/OI", "DDLS/DF", "DDLX/MX", "SRVD/SRV", "BDEF/BDO", "SRVB/SVB", "MSAG/N", "ENHO/EO"],
159
164
  },
160
165
  object_name: {
161
166
  type: "string",
@@ -233,18 +233,19 @@ function detectSystemType(discoveryXml, basisVersion, platform) {
233
233
  if (discoveryXml.includes("bw/modelingtools") || discoveryXml.includes("bw/querydesigner")) {
234
234
  return "SAP_BW";
235
235
  }
236
- // S/4HANA = BASIS 750+ com certas capabilities
237
- if (bv >= 750) {
238
- // Verifica se tem RAP-related collections (mais provável S/4)
239
- if (discoveryXml.includes("behaviordefinitions") || discoveryXml.includes("ServiceBindings")) {
240
- return "ON_PREMISE_S4";
241
- }
242
- }
243
- // ECC ou sistema antigo
236
+ // ECC ou sistema antigo (BASIS < 750 é sempre ECC)
244
237
  if (bv < 750)
245
238
  return "ON_PREMISE_ECC";
246
- // Default para on-premise S/4
247
- return "ON_PREMISE_S4";
239
+ // BASIS 750+: distinguir S/4HANA de ECC EHP8 (ambos têm BASIS 750)
240
+ // S/4HANA sempre inclui collections RAP/Service no discovery XML.
241
+ // ECC 750 NÃO tem esses endpoints — essa é a diferença chave.
242
+ const hasS4Collections = discoveryXml.includes("behaviordefinitions") ||
243
+ discoveryXml.includes("ServiceBindings") ||
244
+ discoveryXml.includes("ddic/srvd/sources");
245
+ if (hasS4Collections)
246
+ return "ON_PREMISE_S4";
247
+ // BASIS 750 sem collections S/4 = ECC EHP8
248
+ return "ON_PREMISE_ECC";
248
249
  }
249
250
  function detectRelease(basisVersion) {
250
251
  const bv = parseInt(basisVersion, 10) || 0;
@@ -29,7 +29,8 @@ async function abapListTransports(input) {
29
29
  });
30
30
  const requests = parseTransportResponse(response.data);
31
31
  if (requests.length === 0) {
32
- return `Nenhuma ordem de transporte aberta encontrada para ${user.toUpperCase()}.`;
32
+ return `Nenhuma ordem de transporte aberta encontrada para ${user.toUpperCase()}.\n` +
33
+ `Para criar uma nova request, use abap_create_transport ou crie manualmente via SE09/SE10 no SAP GUI.`;
33
34
  }
34
35
  const header = `Ordens de transporte abertas — ${user.toUpperCase()} (${requests.length}):\n`;
35
36
  const rows = requests.map((r) => {
@@ -31,6 +31,12 @@ async function abapWrite(input) {
31
31
  await (0, adt_client_js_1.adtPut)(sourcePath, source, etag, transport_request);
32
32
  }
33
33
  catch (putError) {
34
+ // Detectar erro de transport request obrigatória
35
+ if (isTransportRequiredError(putError) && !transport_request) {
36
+ throw new Error(`Objeto ${name} requer transport request para gravação (pacote não-local).\n` +
37
+ `Use abap_list_transports para verificar requests abertas, ou abap_create_transport para criar uma nova.\n` +
38
+ `Depois, informe o parâmetro transport_request (ex: "DEVK900123").`);
39
+ }
34
40
  // Workaround: em on-premise S/4, o GET retorna ETag com sufixo diferente do esperado pelo PUT.
35
41
  // Se o PUT falha com 412, extrai o ETag correto da mensagem de erro e tenta novamente.
36
42
  if ((0, system_profile_js_1.getProfile)().quirks.etagRequiresManualFix && is412Error(putError)) {
@@ -54,6 +60,19 @@ async function abapWrite(input) {
54
60
  }
55
61
  return `Objeto ${object_name} gravado com sucesso.`;
56
62
  }
63
+ /** Detecta erros do SAP indicando que transport request é obrigatória */
64
+ function isTransportRequiredError(error) {
65
+ if (error instanceof adt_client_js_1.AdtError) {
66
+ const msg = (error.sapMessage ?? error.message).toLowerCase();
67
+ return msg.includes("correction") || msg.includes("transport") || msg.includes("request");
68
+ }
69
+ const data = error.response?.data;
70
+ if (typeof data === "string") {
71
+ const lower = data.toLowerCase();
72
+ return lower.includes("correction") || lower.includes("transport request") || lower.includes("corrNr");
73
+ }
74
+ return false;
75
+ }
57
76
  function is412Error(error) {
58
77
  return typeof error === "object" && error !== null &&
59
78
  "response" in error &&
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linkup-ai/abap-ai",
3
- "version": "2.0.0",
4
- "description": "LKPABAP.ia — AI-powered ABAP development tools for SAP S/4HANA via ADT REST API",
3
+ "version": "2.2.0",
4
+ "description": "LKPABAP.ai — AI-powered ABAP development tools for SAP S/4HANA via ADT REST API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "abap-ai": "dist/cli.js"
@@ -11,7 +11,7 @@
11
11
  "README.md"
12
12
  ],
13
13
  "publishConfig": {
14
- "access": "restricted"
14
+ "access": "public"
15
15
  },
16
16
  "license": "SEE LICENSE IN LICENSE",
17
17
  "keywords": [