@linkup-ai/abap-ai 0.1.1 → 0.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.
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+ /**
3
+ * Instalação do ABAP Security Agent no sistema SAP.
4
+ *
5
+ * Cria os objetos ABAP (tabelas, classes) via ADT REST API.
6
+ * Executado após o init, quando o sistema é Enterprise/Corporate.
7
+ *
8
+ * Fluxo:
9
+ * 1. Verifica se os objetos já existem (SELECT COUNT(*) em ZLKP_AI_CONFIG)
10
+ * 2. Se não existem, oferece instalar
11
+ * 3. Cria tabelas, exception, guard class
12
+ * 4. Carrega policy padrão para o environment role
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.checkAgentInstalled = checkAgentInstalled;
49
+ exports.installAgent = installAgent;
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const https = __importStar(require("https"));
53
+ const http = __importStar(require("http"));
54
+ /**
55
+ * Verifica se o Security Agent já está instalado.
56
+ * Tenta ler a tabela ZLKP_AI_CONFIG via SQL Console.
57
+ */
58
+ async function checkAgentInstalled(opts) {
59
+ try {
60
+ const result = await adtRequest(opts, "POST", "/datapreview/freestyle", "SELECT COUNT(*) AS CNT FROM ZLKP_AI_CONFIG", { rowNumber: "1" }, "application/vnd.sap.adt.datapreview.table.v1+xml");
61
+ return result.status >= 200 && result.status < 300;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ /**
68
+ * Instala o Security Agent criando os objetos ABAP via ADT.
69
+ *
70
+ * Ordem:
71
+ * 1. ZCX_LKP_AI_DENIED (exception class — sem dependências)
72
+ * 2. ZLKP_AI_CONFIG (tabela config)
73
+ * 3. ZLKP_AI_AUDIT (tabela audit)
74
+ * 4. ZCL_LKP_AI_GUARD (classe — depende das tabelas + exception)
75
+ *
76
+ * Os fontes ABAP estão em src/abap/security-agent/*.
77
+ */
78
+ async function installAgent(opts) {
79
+ const created = [];
80
+ const errors = [];
81
+ // Obter CSRF token
82
+ const csrf = await fetchCsrf(opts);
83
+ if (!csrf) {
84
+ return { success: false, message: "Falha ao obter CSRF token do SAP.", objects: [] };
85
+ }
86
+ // 1. Criar exception class ZCX_LKP_AI_DENIED
87
+ const exSource = readAbapSource("zcx_lkp_ai_denied.clas.abap");
88
+ const exResult = await createObject(opts, csrf, "CLAS/OC", "ZCX_LKP_AI_DENIED", "LKPABAP.ia: Exception - tool denied", opts.transportRequest);
89
+ if (exResult.ok) {
90
+ await writeSource(opts, csrf, "/oo/classes/ZCX_LKP_AI_DENIED/source/main", exSource);
91
+ await activateObject(opts, csrf, "CLAS/OC", "ZCX_LKP_AI_DENIED");
92
+ created.push("ZCX_LKP_AI_DENIED");
93
+ }
94
+ else if (exResult.exists) {
95
+ created.push("ZCX_LKP_AI_DENIED (já existe)");
96
+ }
97
+ else {
98
+ errors.push(`ZCX_LKP_AI_DENIED: ${exResult.error}`);
99
+ }
100
+ // 2. Criar tabela ZLKP_AI_CONFIG
101
+ // Nota: tabelas transparentes precisam ser criadas via DDIC, não via ADT create+write.
102
+ // O ADT create para TABL/DT cria a estrutura, e o write define os campos.
103
+ const cfgResult = await createObject(opts, csrf, "TABL/DT", "ZLKP_AI_CONFIG", "LKPABAP.ia: Configuração por ambiente", opts.transportRequest);
104
+ if (cfgResult.ok) {
105
+ const cfgSource = readAbapSource("zlkp_ai_config.tabl.asdic");
106
+ await writeSource(opts, csrf, "/ddic/tables/ZLKP_AI_CONFIG/source/main", cfgSource);
107
+ await activateObject(opts, csrf, "TABL/DT", "ZLKP_AI_CONFIG");
108
+ created.push("ZLKP_AI_CONFIG");
109
+ }
110
+ else if (cfgResult.exists) {
111
+ created.push("ZLKP_AI_CONFIG (já existe)");
112
+ }
113
+ else {
114
+ errors.push(`ZLKP_AI_CONFIG: ${cfgResult.error}`);
115
+ }
116
+ // 3. Criar tabela ZLKP_AI_AUDIT
117
+ const audResult = await createObject(opts, csrf, "TABL/DT", "ZLKP_AI_AUDIT", "LKPABAP.ia: Audit Log de Operações", opts.transportRequest);
118
+ if (audResult.ok) {
119
+ const audSource = readAbapSource("zlkp_ai_audit.tabl.asdic");
120
+ await writeSource(opts, csrf, "/ddic/tables/ZLKP_AI_AUDIT/source/main", audSource);
121
+ await activateObject(opts, csrf, "TABL/DT", "ZLKP_AI_AUDIT");
122
+ created.push("ZLKP_AI_AUDIT");
123
+ }
124
+ else if (audResult.exists) {
125
+ created.push("ZLKP_AI_AUDIT (já existe)");
126
+ }
127
+ else {
128
+ errors.push(`ZLKP_AI_AUDIT: ${audResult.error}`);
129
+ }
130
+ // 4. Criar guard class ZCL_LKP_AI_GUARD
131
+ const guardSource = readAbapSource("zcl_lkp_ai_guard.clas.abap");
132
+ const guardResult = await createObject(opts, csrf, "CLAS/OC", "ZCL_LKP_AI_GUARD", "LKPABAP.ia: Security Guard", opts.transportRequest);
133
+ if (guardResult.ok) {
134
+ await writeSource(opts, csrf, "/oo/classes/ZCL_LKP_AI_GUARD/source/main", guardSource);
135
+ await activateObject(opts, csrf, "CLAS/OC", "ZCL_LKP_AI_GUARD");
136
+ created.push("ZCL_LKP_AI_GUARD");
137
+ }
138
+ else if (guardResult.exists) {
139
+ created.push("ZCL_LKP_AI_GUARD (já existe)");
140
+ }
141
+ else {
142
+ errors.push(`ZCL_LKP_AI_GUARD: ${guardResult.error}`);
143
+ }
144
+ if (errors.length > 0) {
145
+ return {
146
+ success: false,
147
+ message: `Instalação parcial. Erros:\n${errors.map((e) => ` - ${e}`).join("\n")}`,
148
+ objects: created,
149
+ };
150
+ }
151
+ return {
152
+ success: true,
153
+ message: `Security Agent instalado com sucesso. ${created.length} objetos criados/verificados.`,
154
+ objects: created,
155
+ };
156
+ }
157
+ // ─── ADT helpers (standalone, sem depender de adt-client.ts) ─────
158
+ function readAbapSource(filename) {
159
+ // Fontes estão em src/abap/security-agent/ (dev) ou dist/abap/security-agent/ (runtime)
160
+ const candidates = [
161
+ path.resolve(__dirname, "..", "abap", "security-agent", filename), // dist/cli/../abap/ = dist/abap/
162
+ path.resolve(__dirname, "abap", "security-agent", filename), // fallback
163
+ path.resolve(__dirname, "..", "..", "src", "abap", "security-agent", filename), // dev mode
164
+ ];
165
+ for (const p of candidates) {
166
+ try {
167
+ return fs.readFileSync(p, "utf-8");
168
+ }
169
+ catch { /* try next */ }
170
+ }
171
+ throw new Error(`Fonte ABAP não encontrado: ${filename}`);
172
+ }
173
+ function getAuth(opts) {
174
+ return "Basic " + Buffer.from(`${opts.user}:${opts.pass}`).toString("base64");
175
+ }
176
+ /** Session cookies capturados no fetchCsrf — necessários em todas as chamadas subsequentes */
177
+ let sessionCookies = "";
178
+ async function fetchCsrf(opts) {
179
+ try {
180
+ const result = await adtRequest(opts, "GET", "/discovery", "", {}, "application/atomsvc+xml", { "X-CSRF-Token": "Fetch" });
181
+ // Capturar cookies da sessão (sap-contextid, SAP_SESSIONID)
182
+ const raw = result.headers["set-cookie"];
183
+ if (raw) {
184
+ const cookies = Array.isArray(raw) ? raw : [raw];
185
+ sessionCookies = cookies.map((c) => c.split(";")[0]).join("; ");
186
+ }
187
+ return result.headers["x-csrf-token"] || null;
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
193
+ async function createObject(opts, csrf, objType, objName, description, transport) {
194
+ const TYPE_TO_PATH = {
195
+ "CLAS/OC": "oo/classes",
196
+ "TABL/DT": "ddic/tables",
197
+ };
198
+ const adtPath = TYPE_TO_PATH[objType];
199
+ if (!adtPath)
200
+ return { ok: false, exists: false, error: `Tipo ${objType} não suportado` };
201
+ const CONTENT_TYPES = {
202
+ "CLAS/OC": "application/vnd.sap.adt.oo.classes.v2+xml",
203
+ "TABL/DT": "application/vnd.sap.adt.tables.v2+xml",
204
+ };
205
+ const NS = {
206
+ "CLAS/OC": `<class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${description}" adtcore:language="${opts.language}" adtcore:name="${objName}" adtcore:responsible="${opts.user}"><adtcore:packageRef adtcore:name="$TMP"/></class:abapClass>`,
207
+ "TABL/DT": `<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${description}" adtcore:language="${opts.language}" adtcore:name="${objName}" adtcore:responsible="${opts.user}"><adtcore:packageRef adtcore:name="$TMP"/></blue:blueSource>`,
208
+ };
209
+ const payload = `<?xml version="1.0" encoding="UTF-8"?>\n${NS[objType]}`;
210
+ const params = {};
211
+ if (transport)
212
+ params.corrNr = transport;
213
+ try {
214
+ const result = await adtRequest(opts, "POST", `/${adtPath}`, payload, params, CONTENT_TYPES[objType], { "X-CSRF-Token": csrf, "Content-Type": CONTENT_TYPES[objType] });
215
+ if (result.status === 200 || result.status === 201) {
216
+ return { ok: true, exists: false };
217
+ }
218
+ if (result.status === 409 || result.body.includes("already exists") || result.body.includes("já existe")) {
219
+ return { ok: false, exists: true };
220
+ }
221
+ return { ok: false, exists: false, error: `HTTP ${result.status}: ${result.body.substring(0, 200)}` };
222
+ }
223
+ catch (e) {
224
+ return { ok: false, exists: false, error: e.message };
225
+ }
226
+ }
227
+ async function writeSource(opts, csrf, adtPath, source) {
228
+ // GET etag primeiro
229
+ const getResult = await adtRequest(opts, "GET", adtPath, "", {}, "text/plain");
230
+ const etag = getResult.headers["etag"] || "";
231
+ await adtRequest(opts, "PUT", adtPath, source, {}, "text/plain", {
232
+ "X-CSRF-Token": csrf,
233
+ "Content-Type": "text/plain",
234
+ "If-Match": etag,
235
+ });
236
+ }
237
+ async function activateObject(opts, csrf, objType, objName) {
238
+ const TYPE_TO_PATH = { "CLAS/OC": "oo/classes", "TABL/DT": "ddic/tables" };
239
+ const adtPath = TYPE_TO_PATH[objType];
240
+ const adtUri = `/sap/bc/adt/${adtPath}/${objName}`;
241
+ const payload = `<?xml version="1.0" encoding="UTF-8"?><adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core"><adtcore:objectReference adtcore:uri="${adtUri}" adtcore:name="${objName}"/></adtcore:objectReferences>`;
242
+ await adtRequest(opts, "POST", "/activation", payload, {}, "application/xml", {
243
+ "X-CSRF-Token": csrf,
244
+ "Content-Type": "application/xml",
245
+ });
246
+ }
247
+ function adtRequest(opts, method, adtPath, body, params, accept, extraHeaders) {
248
+ const baseUrl = new URL(`${opts.url}/sap/bc/adt${adtPath}`);
249
+ baseUrl.searchParams.set("sap-client", opts.client);
250
+ baseUrl.searchParams.set("sap-language", opts.language);
251
+ for (const [k, v] of Object.entries(params)) {
252
+ baseUrl.searchParams.set(k, v);
253
+ }
254
+ const headers = {
255
+ Authorization: getAuth(opts),
256
+ Accept: accept,
257
+ ...(sessionCookies ? { Cookie: sessionCookies } : {}),
258
+ ...extraHeaders,
259
+ };
260
+ const transport = baseUrl.protocol === "https:" ? https : http;
261
+ const requestOpts = {
262
+ hostname: baseUrl.hostname,
263
+ port: baseUrl.port || (baseUrl.protocol === "https:" ? 443 : 80),
264
+ path: baseUrl.pathname + baseUrl.search,
265
+ method,
266
+ headers,
267
+ timeout: 30000,
268
+ };
269
+ if (opts.selfSignedSsl) {
270
+ requestOpts.rejectUnauthorized = false;
271
+ }
272
+ return new Promise((resolve, reject) => {
273
+ const req = transport.request(requestOpts, (res) => {
274
+ let data = "";
275
+ res.on("data", (chunk) => (data += chunk));
276
+ res.on("end", () => {
277
+ resolve({
278
+ status: res.statusCode || 0,
279
+ body: data,
280
+ headers: res.headers,
281
+ });
282
+ });
283
+ });
284
+ req.on("error", reject);
285
+ req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
286
+ if (body)
287
+ req.write(body);
288
+ req.end();
289
+ });
290
+ }
package/dist/cli.js CHANGED
@@ -17,6 +17,8 @@ const HELP = `
17
17
 
18
18
  Comandos:
19
19
  init [--local] Configura conexão com sistema SAP (wizard interativo)
20
+ init --name dev --url https://host:8443 --user USER --pass PASS [opts]
21
+ Modo non-interactive (para automação/CI)
20
22
  link Vincula sistemas globais a este workspace (VS Code)
21
23
  activate <LICENSE_KEY> Ativa licença do produto
22
24
  status Mostra licença, sistemas e métricas de uso
@@ -50,9 +52,25 @@ async function main() {
50
52
  }
51
53
  const local = args.includes("--local");
52
54
  switch (command) {
53
- case "init":
54
- await (0, init_js_1.init)({ local });
55
+ case "init": {
56
+ const getFlag = (flag) => {
57
+ const idx = args.indexOf(flag);
58
+ return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined;
59
+ };
60
+ await (0, init_js_1.init)({
61
+ local,
62
+ name: getFlag("--name"),
63
+ url: getFlag("--url"),
64
+ client: getFlag("--client"),
65
+ user: getFlag("--user"),
66
+ pass: getFlag("--pass"),
67
+ language: getFlag("--language"),
68
+ selfSignedSsl: args.includes("--no-ssl-verify") ? true : undefined,
69
+ envRole: getFlag("--env-role"),
70
+ skipTest: args.includes("--skip-test"),
71
+ });
55
72
  break;
73
+ }
56
74
  case "activate": {
57
75
  const key = args[1];
58
76
  if (!key) {
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ const function_group_js_1 = require("./tools/function-group.js");
29
29
  const message_class_js_1 = require("./tools/message-class.js");
30
30
  const abap_doc_js_1 = require("./tools/abap-doc.js");
31
31
  const release_transport_js_1 = require("./tools/release-transport.js");
32
- const delete_js_1 = require("./tools/delete.js");
32
+ // abapDeleteObject removido — delete bloqueado permanentemente (decisão de produto)
33
33
  const refactor_rename_js_1 = require("./tools/refactor-rename.js");
34
34
  const code_completion_js_1 = require("./tools/code-completion.js");
35
35
  const navigate_js_1 = require("./tools/navigate.js");
@@ -56,8 +56,10 @@ const breakpoints_js_1 = require("./tools/breakpoints.js");
56
56
  const knowledge_js_1 = require("./tools/knowledge.js");
57
57
  const create_dcl_js_1 = require("./tools/create-dcl.js");
58
58
  const create_amdp_js_1 = require("./tools/create-amdp.js");
59
+ const execute_js_1 = require("./tools/execute.js");
59
60
  const system_profile_js_1 = require("./system-profile.js");
60
61
  const security_policy_js_1 = require("./security-policy.js");
62
+ const sap_policy_loader_js_1 = require("./sap-policy-loader.js");
61
63
  const security_audit_js_1 = require("./security-audit.js");
62
64
  const license_guard_js_1 = require("./license-guard.js");
63
65
  const server = new mcp_js_1.McpServer({
@@ -817,35 +819,22 @@ server.tool("abap_release_transport", "Libera uma ordem de transporte no SAP. AT
817
819
  };
818
820
  }
819
821
  });
820
- // Tool: abap_delete
821
- server.tool("abap_delete", "Exclui um objeto ABAP do sistema SAP. ATENÇÃO: operação irreversível. Requer transporte para objetos em pacotes não-locais.", {
822
- object_type: zod_1.z
823
- .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"])
824
- .describe("Tipo do objeto SAP a excluir."),
825
- object_name: zod_1.z
826
- .string()
827
- .describe("Nome do objeto ABAP a excluir."),
828
- transport_request: zod_1.z
829
- .string()
830
- .optional()
831
- .describe("Ordem de transporte (obrigatória para objetos em pacotes não-locais)."),
832
- }, async ({ object_type, object_name, transport_request }) => {
833
- const blocked = securityGuard("abap_delete", transport_request);
834
- if (blocked)
835
- return blocked;
836
- try {
837
- const result = await (0, delete_js_1.abapDeleteObject)({ object_type, object_name, transport_request });
838
- return {
839
- content: [{ type: "text", text: result }],
840
- };
841
- }
842
- catch (error) {
843
- const message = error instanceof Error ? error.message : String(error);
844
- return {
845
- content: [{ type: "text", text: `Erro ao excluir objeto: ${message}` }],
846
- isError: true,
847
- };
848
- }
822
+ // Tool: abap_delete — BLOQUEADA permanentemente (decisão de produto)
823
+ server.tool("abap_delete", "Exclusão de objetos ABAP não é suportada pelo LKPABAP.ia. Use a transação SE80 no SAP GUI ou o Eclipse ADT para excluir objetos.", {
824
+ object_type: zod_1.z.string().describe("Tipo do objeto SAP."),
825
+ object_name: zod_1.z.string().describe("Nome do objeto ABAP."),
826
+ }, async () => {
827
+ return {
828
+ content: [{
829
+ type: "text",
830
+ text: "BLOQUEADO: O LKPABAP.ia não executa exclusão de objetos ABAP.\n\n"
831
+ + "Exclusão é uma operação irreversível que deve ser feita manualmente pelo desenvolvedor:\n"
832
+ + " • SAP GUI: transação SE80 → selecionar objeto → Excluir\n"
833
+ + " Eclipse ADT: botão direito Delete\n\n"
834
+ + "Esta restrição é permanente e não pode ser sobreposta por configuração.",
835
+ }],
836
+ isError: true,
837
+ };
849
838
  });
850
839
  // Tool: abap_refactor_rename
851
840
  server.tool("abap_refactor_rename", "Renomeia um símbolo (variável, método, atributo) em um objeto ABAP usando o refactoring ADT. Aplica a renomeação em todas as referências.", {
@@ -1415,6 +1404,31 @@ server.resource("system-profile", "abap://system-profile", { description: "Perfi
1415
1404
  }],
1416
1405
  };
1417
1406
  });
1407
+ // Tool: abap_execute — executa programas ABAP via ICF handler
1408
+ server.tool("abap_execute", "Executa um programa ABAP (Z* ou Y*) no sistema SAP e retorna o output. Requer ICF handler /sap/z/lkpai/guard ativado no SICF. Útil para programas de setup, migração, testes ou utilities.", {
1409
+ program_name: zod_1.z
1410
+ .string()
1411
+ .describe("Nome do programa ABAP a executar (ex: ZLKP_AI_SETUP). Apenas Z* e Y*."),
1412
+ variant: zod_1.z
1413
+ .string()
1414
+ .optional()
1415
+ .describe("Variante de seleção (opcional). Se não informada, executa com valores default dos parâmetros."),
1416
+ }, async ({ program_name, variant }) => {
1417
+ const blocked = securityGuard("abap_execute");
1418
+ if (blocked)
1419
+ return blocked;
1420
+ try {
1421
+ const result = await (0, execute_js_1.abapExecute)({ program_name, variant });
1422
+ return { content: [{ type: "text", text: result }] };
1423
+ }
1424
+ catch (error) {
1425
+ const message = error instanceof Error ? error.message : String(error);
1426
+ return {
1427
+ content: [{ type: "text", text: `Erro ao executar programa: ${message}` }],
1428
+ isError: true,
1429
+ };
1430
+ }
1431
+ });
1418
1432
  // Resource: security-policy
1419
1433
  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 () => {
1420
1434
  const policy = (0, security_policy_js_1.getPolicy)();
@@ -1440,10 +1454,24 @@ async function main() {
1440
1454
  (0, security_policy_js_1.validateToolRiskMap)(_registeredToolNames);
1441
1455
  const transport = new stdio_js_1.StdioServerTransport();
1442
1456
  await server.connect(transport);
1457
+ // Tentar carregar policy do SAP (ABAP Security Agent)
1458
+ // Se o agente está instalado (ZLKP_AI_CONFIG existe), a policy SAP tem prioridade
1459
+ const sapUser = process.env.SAP_USER || "";
1460
+ let policySource = "local";
1461
+ try {
1462
+ const sapPolicy = await (0, sap_policy_loader_js_1.loadPolicyFromSap)(sapUser);
1463
+ if (sapPolicy) {
1464
+ (0, security_policy_js_1.setPolicy)(sapPolicy);
1465
+ policySource = "SAP (ZLKP_AI_CONFIG)";
1466
+ }
1467
+ }
1468
+ catch {
1469
+ // Fallback silencioso para policy local
1470
+ }
1443
1471
  const profile = (0, system_profile_js_1.getProfile)();
1444
1472
  const policy = (0, security_policy_js_1.getPolicy)();
1445
1473
  const licLabel = (0, license_guard_js_1.licenseStatusLabel)();
1446
- process.stderr.write(`abap-mcp-server v${VERSION} iniciado | License: ${licLabel} | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")})\n`);
1474
+ process.stderr.write(`abap-mcp-server v${VERSION} iniciado | License: ${licLabel} | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")}) [${policySource}]\n`);
1447
1475
  }
1448
1476
  main().catch((err) => {
1449
1477
  process.stderr.write(`Falha ao iniciar servidor: ${err}\n`);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * License Guard — valida licença antes de executar qualquer tool.
3
+ * License Guard — valida licença e rate limiting antes de executar qualquer tool.
4
4
  *
5
5
  * Singleton: lê ~/.abap-ai/license.json uma vez e cacheia.
6
6
  * licenseGuard() retorna null se OK, ou MCP error response se bloqueado.
@@ -11,6 +11,7 @@ exports.reloadLicense = reloadLicense;
11
11
  exports.licenseGuard = licenseGuard;
12
12
  exports.licenseStatusLabel = licenseStatusLabel;
13
13
  const activate_js_1 = require("./cli/activate.js");
14
+ const security_audit_js_1 = require("./security-audit.js");
14
15
  // ─── Singleton ───────────────────────────────────────────────────
15
16
  let cachedLicense = undefined;
16
17
  /**
@@ -65,8 +66,49 @@ function licenseGuard() {
65
66
  isError: true,
66
67
  };
67
68
  }
69
+ // Rate limiting (Starter: 200 calls/dia)
70
+ const limit = license.limits.calls_per_day;
71
+ if (limit > 0) {
72
+ const used = getDailyCallCount();
73
+ if (used >= limit) {
74
+ return {
75
+ content: [{
76
+ type: "text",
77
+ text: `LKPABAP.ia — Limite diário atingido (${used}/${limit} chamadas).\n\n` +
78
+ `Seu plano ${license.plan} permite ${limit} chamadas por dia.\n` +
79
+ `O contador reseta à meia-noite.\n\n` +
80
+ `Para uso ilimitado, faça upgrade para o plano Pro:\n` +
81
+ ` https://lkpabap.ai/pricing`,
82
+ }],
83
+ isError: true,
84
+ };
85
+ }
86
+ }
68
87
  return null;
69
88
  }
89
+ // ─── Rate Limiting ──────────────────────────────────────────────
90
+ /** Cache do count diário para não reler o arquivo a cada chamada */
91
+ let dailyCountCache = null;
92
+ /**
93
+ * Conta chamadas ALLOWED do dia (audit log do dia + sessão atual).
94
+ * Cache é invalidado quando o dia muda.
95
+ */
96
+ function getDailyCallCount() {
97
+ const today = new Date().toISOString().slice(0, 10);
98
+ // Invalidar cache se mudou o dia
99
+ if (dailyCountCache && dailyCountCache.date !== today) {
100
+ dailyCountCache = null;
101
+ }
102
+ // Ler do arquivo apenas 1x por sessão (chamadas anteriores de hoje)
103
+ if (!dailyCountCache) {
104
+ const todayEntries = (0, security_audit_js_1.readTodayAudit)();
105
+ const previousAllowed = todayEntries.filter((e) => e.result === "ALLOWED").length;
106
+ dailyCountCache = { date: today, count: previousAllowed };
107
+ }
108
+ // Incrementar com a chamada atual (este guard roda ANTES do audit, então +1)
109
+ dailyCountCache.count++;
110
+ return dailyCountCache.count;
111
+ }
70
112
  /**
71
113
  * Retorna string de status para log no startup.
72
114
  */