@linkup-ai/abap-ai 2.0.0 → 2.1.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 |
@@ -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.ia 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
  // ---------------------------------------------------------------------------
@@ -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)}│
@@ -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/index.js CHANGED
@@ -55,10 +55,42 @@ 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");
58
60
  const server = new mcp_js_1.McpServer({
59
61
  name: "abap-adt",
60
62
  version: "2.0.0",
61
63
  });
64
+ // ─── Security Guard ───────────────────────────────────────────────
65
+ /**
66
+ * Verifica se uma tool pode ser executada sob a security policy atual.
67
+ * Retorna null se permitido, ou uma resposta de erro MCP se bloqueado.
68
+ */
69
+ function securityGuard(toolName, transportRequest) {
70
+ const policy = (0, security_policy_js_1.getPolicy)();
71
+ const riskLevel = (0, security_policy_js_1.getToolRisk)(toolName);
72
+ // 1. Verificar acesso pela policy
73
+ const accessCheck = (0, security_policy_js_1.checkToolAccess)(toolName, policy);
74
+ if (!accessCheck.allowed) {
75
+ (0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, accessCheck.reason || "policy");
76
+ return {
77
+ content: [{ type: "text", text: accessCheck.reason || `Tool ${toolName} bloqueada.` }],
78
+ isError: true,
79
+ };
80
+ }
81
+ // 2. Verificar transport request obrigatório
82
+ const transportCheck = (0, security_policy_js_1.checkTransportRequired)(toolName, transportRequest, policy);
83
+ if (!transportCheck.allowed) {
84
+ (0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, transportCheck.reason || "transport required");
85
+ return {
86
+ content: [{ type: "text", text: transportCheck.reason || `Transport request obrigatório.` }],
87
+ isError: true,
88
+ };
89
+ }
90
+ // 3. Permitido — registrar no audit
91
+ (0, security_audit_js_1.auditAllowed)(toolName, riskLevel, policy.environment_role);
92
+ return null;
93
+ }
62
94
  // Tool: abap_read
63
95
  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
96
  object_type: zod_1.z
@@ -106,6 +138,9 @@ server.tool("abap_write", "Grava código-fonte em um objeto ABAP existente no si
106
138
  .optional()
107
139
  .describe("Ordem de transporte (ex: ECDK900123). Obrigatória para objetos em pacotes não-locais."),
108
140
  }, async ({ object_type, object_name, source, class_include, transport_request }) => {
141
+ const blocked = securityGuard("abap_write", transport_request);
142
+ if (blocked)
143
+ return blocked;
109
144
  try {
110
145
  const result = await (0, write_js_1.abapWrite)({ object_type, object_name, source, class_include, transport_request });
111
146
  return {
@@ -129,6 +164,9 @@ server.tool("abap_activate", "Ativa um objeto ABAP no sistema SAP via ADT API. R
129
164
  .string()
130
165
  .describe("Nome do objeto ABAP (ex: ZTESTEDENIS)."),
131
166
  }, async ({ object_type, object_name }) => {
167
+ const blocked = securityGuard("abap_activate");
168
+ if (blocked)
169
+ return blocked;
132
170
  try {
133
171
  const result = await (0, activate_js_1.abapActivate)({ object_type, object_name });
134
172
  return {
@@ -173,6 +211,9 @@ server.tool("abap_create", "Cria um novo objeto ABAP no sistema SAP via ADT API.
173
211
  .optional()
174
212
  .describe("Ordem de transporte (ex: ECDK900123). Se omitida, o SAP usa $TMP ou atribui automaticamente."),
175
213
  }, async ({ object_type, object_name, description, package: pkg, srvd_name, binding_type, transport_request }) => {
214
+ const blocked = securityGuard("abap_create", transport_request);
215
+ if (blocked)
216
+ return blocked;
176
217
  try {
177
218
  const result = await (0, create_js_1.abapCreate)({ object_type, object_name, description, package: pkg, srvd_name, binding_type, transport_request });
178
219
  return {
@@ -302,6 +343,9 @@ server.tool("abap_assign_transport", "Vincula um objeto ABAP existente a uma ord
302
343
  .string()
303
344
  .describe("Nome do objeto ABAP a vincular."),
304
345
  }, async ({ transport_request, object_type, object_name }) => {
346
+ const blocked = securityGuard("abap_assign_transport", transport_request);
347
+ if (blocked)
348
+ return blocked;
305
349
  try {
306
350
  const result = await (0, transports_js_1.abapAssignTransport)({ transport_request, object_type, object_name });
307
351
  return {
@@ -358,6 +402,9 @@ server.tool("abap_scaffold_rap", "Cria um stack RAP completo no SAP com 4 scenar
358
402
  .optional()
359
403
  .describe("Ordem de transporte para todos os objetos criados."),
360
404
  }, async ({ base_name, description, scenario, child_entities, with_feature_control, with_business_events, package: pkg, binding_type, transport_request }) => {
405
+ const blocked = securityGuard("abap_scaffold_rap", transport_request);
406
+ if (blocked)
407
+ return blocked;
361
408
  try {
362
409
  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
410
  return {
@@ -382,6 +429,9 @@ server.tool("abap_publish_binding", "Publica uma Service Binding (SRVB/SVB) no S
382
429
  .optional()
383
430
  .describe("Versão do serviço no Gateway (padrão: '0001')."),
384
431
  }, async ({ binding_name, service_version }) => {
432
+ const blocked = securityGuard("abap_publish_binding");
433
+ if (blocked)
434
+ return blocked;
385
435
  try {
386
436
  const result = await (0, publish_binding_js_1.abapPublishBinding)({ binding_name, service_version });
387
437
  return {
@@ -417,6 +467,9 @@ server.tool("abap_deploy_bsp", "Faz deploy de uma aplicação Fiori/UI5 para o A
417
467
  .optional()
418
468
  .describe("Ordem de transporte (ex: ECDK900123). Obrigatória se o pacote não for $TMP."),
419
469
  }, async ({ bsp_name, folder_path, package: pkg, description, transport_request }) => {
470
+ const blocked = securityGuard("abap_deploy_bsp", transport_request);
471
+ if (blocked)
472
+ return blocked;
420
473
  try {
421
474
  const result = await (0, deploy_bsp_js_1.abapDeployBsp)({ bsp_name, folder_path, package: pkg, description, transport_request });
422
475
  return {
@@ -492,8 +545,9 @@ server.tool("abap_data_preview", "Visualiza dados de uma tabela transparente ou
492
545
  .optional()
493
546
  .describe("Colunas a retornar, separadas por vírgula (ex: MATNR,MTART,MBRSH)."),
494
547
  }, async ({ object_name, max_rows, where_clause, order_by, columns }) => {
548
+ const effectiveRows = (0, security_policy_js_1.getEffectiveMaxRows)(max_rows ?? 100, (0, security_policy_js_1.getPolicy)());
495
549
  try {
496
- const result = await (0, data_preview_js_1.abapDataPreview)({ object_name, max_rows, where_clause, order_by, columns });
550
+ const result = await (0, data_preview_js_1.abapDataPreview)({ object_name, max_rows: effectiveRows, where_clause, order_by, columns });
497
551
  return {
498
552
  content: [{ type: "text", text: result }],
499
553
  };
@@ -721,6 +775,9 @@ server.tool("abap_release_transport", "Libera uma ordem de transporte no SAP. AT
721
775
  .string()
722
776
  .describe("Número da ordem de transporte a liberar (ex: ECDK900123)."),
723
777
  }, async ({ transport_request }) => {
778
+ const blocked = securityGuard("abap_release_transport", transport_request);
779
+ if (blocked)
780
+ return blocked;
724
781
  try {
725
782
  const result = await (0, release_transport_js_1.abapReleaseTransport)({ transport_request });
726
783
  return {
@@ -748,6 +805,9 @@ server.tool("abap_delete", "Exclui um objeto ABAP do sistema SAP. ATENÇÃO: ope
748
805
  .optional()
749
806
  .describe("Ordem de transporte (obrigatória para objetos em pacotes não-locais)."),
750
807
  }, async ({ object_type, object_name, transport_request }) => {
808
+ const blocked = securityGuard("abap_delete", transport_request);
809
+ if (blocked)
810
+ return blocked;
751
811
  try {
752
812
  const result = await (0, delete_js_1.abapDeleteObject)({ object_type, object_name, transport_request });
753
813
  return {
@@ -787,6 +847,9 @@ server.tool("abap_refactor_rename", "Renomeia um símbolo (variável, método, a
787
847
  .optional()
788
848
  .describe("Include da classe. Apenas para CLAS/OC. Padrão: main."),
789
849
  }, async ({ object_type, object_name, old_name, new_name, line, column, class_include }) => {
850
+ const blocked = securityGuard("abap_refactor_rename");
851
+ if (blocked)
852
+ return blocked;
790
853
  try {
791
854
  const result = await (0, refactor_rename_js_1.abapRefactorRename)({ object_type, object_name, old_name, new_name, line, column, class_include });
792
855
  return {
@@ -879,6 +942,9 @@ server.tool("abap_quick_fix", "Obtém e aplica correções rápidas (quick fixes
879
942
  .describe("Include da classe. Apenas para CLAS/OC."),
880
943
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte para aplicar o fix."),
881
944
  }, async ({ object_type, object_name, line, column, fix_index, class_include, transport_request }) => {
945
+ const blocked = securityGuard("abap_quick_fix", transport_request);
946
+ if (blocked)
947
+ return blocked;
882
948
  try {
883
949
  const result = await (0, quick_fix_js_1.abapQuickFix)({ object_type, object_name, line, column, fix_index, class_include, transport_request });
884
950
  return { content: [{ type: "text", text: result }] };
@@ -903,6 +969,9 @@ server.tool("abap_extract_method", "Extrai um trecho de código ABAP selecionado
903
969
  .describe("Include da classe. Apenas para CLAS/OC."),
904
970
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte (se necessário)."),
905
971
  }, async ({ object_type, object_name, start_line, end_line, new_method_name, class_include, transport_request }) => {
972
+ const blocked = securityGuard("abap_extract_method", transport_request);
973
+ if (blocked)
974
+ return blocked;
906
975
  try {
907
976
  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
977
  return { content: [{ type: "text", text: result }] };
@@ -933,8 +1002,9 @@ server.tool("abap_sql_console", "Executa uma consulta ABAP SQL (Open SQL) no sis
933
1002
  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
1003
  max_rows: zod_1.z.number().optional().describe("Número máximo de linhas retornadas (padrão: 100)."),
935
1004
  }, async ({ sql, max_rows }) => {
1005
+ const effectiveRows = (0, security_policy_js_1.getEffectiveMaxRows)(max_rows ?? 100, (0, security_policy_js_1.getPolicy)());
936
1006
  try {
937
- const result = await (0, sql_console_js_1.abapSqlConsole)({ sql, max_rows });
1007
+ const result = await (0, sql_console_js_1.abapSqlConsole)({ sql, max_rows: effectiveRows });
938
1008
  return { content: [{ type: "text", text: result }] };
939
1009
  }
940
1010
  catch (error) {
@@ -961,6 +1031,9 @@ server.tool("abap_create_transport", "Cria uma nova ordem de transporte (workben
961
1031
  type: zod_1.z.enum(["W", "K"]).optional().describe("Tipo: W=Workbench (padrão), K=Customizing."),
962
1032
  target: zod_1.z.string().optional().describe("Sistema de destino (ex: QAS). Opcional."),
963
1033
  }, async ({ description, type, target }) => {
1034
+ const blocked = securityGuard("abap_create_transport");
1035
+ if (blocked)
1036
+ return blocked;
964
1037
  try {
965
1038
  const result = await (0, create_transport_js_1.abapCreateTransport)({ description, type, target });
966
1039
  return { content: [{ type: "text", text: result }] };
@@ -1144,6 +1217,9 @@ server.tool("abap_git_pull", "Executa pull de um repositório abapGit no sistema
1144
1217
  branch: zod_1.z.string().optional().describe("Nome do branch (padrão: main)."),
1145
1218
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte para os objetos importados."),
1146
1219
  }, async ({ repo_id, branch, transport_request }) => {
1220
+ const blocked = securityGuard("abapgit_pull", transport_request);
1221
+ if (blocked)
1222
+ return blocked;
1147
1223
  try {
1148
1224
  const result = await (0, abapgit_js_1.abapGitPull)({ repo_id, branch, transport_request });
1149
1225
  return { content: [{ type: "text", text: result }] };
@@ -1163,6 +1239,9 @@ server.tool("abap_git_stage", "Mostra status de staging e executa push para um r
1163
1239
  message: zod_1.z.string().optional().describe("Mensagem de commit (obrigatória para push)."),
1164
1240
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
1165
1241
  }, async ({ repo_id, action, message: commitMsg, transport_request }) => {
1242
+ const blocked = securityGuard("abapgit_stage", transport_request);
1243
+ if (blocked)
1244
+ return blocked;
1166
1245
  try {
1167
1246
  const result = await (0, abapgit_js_1.abapGitStage)({ repo_id, action, message: commitMsg, transport_request });
1168
1247
  return { content: [{ type: "text", text: result }] };
@@ -1233,6 +1312,9 @@ server.tool("abap_create_dcl", "Cria um objeto DCL (Data Control Language) para
1233
1312
  package: zod_1.z.string().optional().describe("Pacote SAP. Padrão: '$TMP'."),
1234
1313
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
1235
1314
  }, async ({ dcl_name, cds_entity, auth_type, auth_mappings, literal_conditions, with_user_aspect, package: pkg, transport_request }) => {
1315
+ const blocked = securityGuard("abap_create_dcl", transport_request);
1316
+ if (blocked)
1317
+ return blocked;
1236
1318
  try {
1237
1319
  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
1320
  return { content: [{ type: "text", text: result }] };
@@ -1262,6 +1344,9 @@ server.tool("abap_create_amdp", "Cria uma classe AMDP (ABAP Managed Database Pro
1262
1344
  package: zod_1.z.string().optional().describe("Pacote SAP. Padrão: '$TMP'."),
1263
1345
  transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
1264
1346
  }, async ({ class_name, method_name, method_type, importing: imp, exporting_table, cds_entity, description, package: pkg, transport_request }) => {
1347
+ const blocked = securityGuard("abap_create_amdp", transport_request);
1348
+ if (blocked)
1349
+ return blocked;
1265
1350
  try {
1266
1351
  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
1352
  return { content: [{ type: "text", text: result }] };
@@ -1305,12 +1390,31 @@ server.resource("system-profile", "abap://system-profile", { description: "Perfi
1305
1390
  }],
1306
1391
  };
1307
1392
  });
1393
+ // Resource: security-policy
1394
+ 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 () => {
1395
+ const policy = (0, security_policy_js_1.getPolicy)();
1396
+ const toolRiskMap = {
1397
+ blocked_tools: policy.always_blocked,
1398
+ environment: policy.environment_role,
1399
+ allowed_operations: policy.allowed_levels,
1400
+ require_transport: policy.require_transport,
1401
+ max_preview_rows: policy.max_preview_rows,
1402
+ };
1403
+ return {
1404
+ contents: [{
1405
+ uri: "abap://security-policy",
1406
+ mimeType: "application/json",
1407
+ text: JSON.stringify(toolRiskMap, null, 2),
1408
+ }],
1409
+ };
1410
+ });
1308
1411
  // Inicializa o servidor via stdio (modo Claude Code MCP)
1309
1412
  async function main() {
1310
1413
  const transport = new stdio_js_1.StdioServerTransport();
1311
1414
  await server.connect(transport);
1312
1415
  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`);
1416
+ const policy = (0, security_policy_js_1.getPolicy)();
1417
+ process.stderr.write(`abap-mcp-server v2.1.0 iniciado | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")})\n`);
1314
1418
  }
1315
1419
  main().catch((err) => {
1316
1420
  process.stderr.write(`Falha ao iniciar servidor: ${err}\n`);
@@ -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.ia 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.ia. `
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.ia 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.ia 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkup-ai/abap-ai",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "LKPABAP.ia — AI-powered ABAP development tools for SAP S/4HANA via ADT REST API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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": [