@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.
- package/dist/abap/security-agent/README.md +30 -0
- package/dist/abap/security-agent/zcl_lkp_ai_guard.clas.abap +282 -0
- package/dist/abap/security-agent/zcl_lkp_ai_icf_handler.clas.abap +275 -0
- package/dist/abap/security-agent/zcx_lkp_ai_denied.clas.abap +50 -0
- package/dist/abap/security-agent/zlkp_ai_audit.tabl.asdic +19 -0
- package/dist/abap/security-agent/zlkp_ai_config.tabl.asdic +21 -0
- package/dist/abap/security-agent/zlkp_ai_setup.prog.abap +359 -0
- package/dist/adt-client.js +63 -74
- package/dist/cli/init.js +40 -6
- package/dist/cli/install-agent.js +290 -0
- package/dist/cli.js +20 -2
- package/dist/index.js +59 -31
- package/dist/license-guard.js +43 -1
- package/dist/sap-policy-loader.js +255 -0
- package/dist/security-policy.js +41 -12
- package/dist/tests/create-payload.test.js +213 -0
- package/dist/tests/security-policy.test.js +167 -0
- package/dist/tool-handler.js +80 -0
- package/dist/tools/activate.js +27 -4
- package/dist/tools/create.js +2 -0
- package/dist/tools/delete.js +12 -8
- package/dist/tools/execute.js +112 -0
- package/dist/tools/transports.js +4 -3
- package/package.json +7 -4
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Testes unitários para security-policy.ts — classificação de risco,
|
|
4
|
+
* validação de tools e enforcement de policy por ambiente.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const vitest_1 = require("vitest");
|
|
8
|
+
const security_policy_js_1 = require("../security-policy.js");
|
|
9
|
+
(0, vitest_1.describe)("getToolRisk", () => {
|
|
10
|
+
(0, vitest_1.it)("retorna READ para tools de leitura", () => {
|
|
11
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_read")).toBe("READ");
|
|
12
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_search")).toBe("READ");
|
|
13
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_knowledge")).toBe("READ");
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)("retorna WRITE para tools de escrita", () => {
|
|
16
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_write")).toBe("WRITE");
|
|
17
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_activate")).toBe("WRITE");
|
|
18
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_git_pull")).toBe("WRITE");
|
|
19
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_git_stage")).toBe("WRITE");
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.it)("retorna CREATE para tools de criação", () => {
|
|
22
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_create")).toBe("CREATE");
|
|
23
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_scaffold_rap")).toBe("CREATE");
|
|
24
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_create_dcl")).toBe("CREATE");
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.it)("retorna ADMIN para operações bloqueadas permanentemente", () => {
|
|
27
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_release_transport")).toBe("ADMIN");
|
|
28
|
+
(0, vitest_1.expect)((0, security_policy_js_1.getToolRisk)("abap_delete")).toBe("ADMIN");
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)("lança erro para tool não classificada", () => {
|
|
31
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.getToolRisk)("abap_inexistente_xyz"))
|
|
32
|
+
.toThrow("[SECURITY]");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.describe)("validateToolRiskMap", () => {
|
|
36
|
+
(0, vitest_1.it)("passa quando todas as tools estão classificadas", () => {
|
|
37
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.validateToolRiskMap)(["abap_read", "abap_write", "abap_create"]))
|
|
38
|
+
.not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)("falha listando tools não classificadas", () => {
|
|
41
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.validateToolRiskMap)(["abap_read", "tool_fake_1", "tool_fake_2"]))
|
|
42
|
+
.toThrow("2 tool(s)");
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)("inclui nome da tool faltante na mensagem", () => {
|
|
45
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.validateToolRiskMap)(["abap_read", "minha_tool_nova"]))
|
|
46
|
+
.toThrow("minha_tool_nova");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.describe)("checkToolAccess", () => {
|
|
50
|
+
(0, vitest_1.describe)("ambiente DEVELOPMENT", () => {
|
|
51
|
+
const policy = (0, security_policy_js_1.getDefaultPolicy)("DEVELOPMENT");
|
|
52
|
+
(0, vitest_1.it)("permite READ, WRITE, CREATE", () => {
|
|
53
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_read", policy).allowed).toBe(true);
|
|
54
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_write", policy).allowed).toBe(true);
|
|
55
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_create", policy).allowed).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.it)("bloqueia abap_release_transport SEMPRE", () => {
|
|
58
|
+
const result = (0, security_policy_js_1.checkToolAccess)("abap_release_transport", policy);
|
|
59
|
+
(0, vitest_1.expect)(result.allowed).toBe(false);
|
|
60
|
+
(0, vitest_1.expect)(result.reason).toContain("sempre bloqueado");
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)("bloqueia abap_delete SEMPRE (mesmo em DEV)", () => {
|
|
63
|
+
const result = (0, security_policy_js_1.checkToolAccess)("abap_delete", policy);
|
|
64
|
+
(0, vitest_1.expect)(result.allowed).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.describe)("ambiente QUALITY", () => {
|
|
68
|
+
const policy = (0, security_policy_js_1.getDefaultPolicy)("QUALITY");
|
|
69
|
+
(0, vitest_1.it)("permite READ", () => {
|
|
70
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_read", policy).allowed).toBe(true);
|
|
71
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_search", policy).allowed).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("bloqueia WRITE", () => {
|
|
74
|
+
const result = (0, security_policy_js_1.checkToolAccess)("abap_write", policy);
|
|
75
|
+
(0, vitest_1.expect)(result.allowed).toBe(false);
|
|
76
|
+
(0, vitest_1.expect)(result.reason).toContain("QUALITY");
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.it)("bloqueia CREATE", () => {
|
|
79
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_create", policy).allowed).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)("bloqueia delete (ADMIN — bloqueado em TODOS os ambientes)", () => {
|
|
82
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_delete", policy).allowed).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.describe)("ambiente PRODUCTION", () => {
|
|
86
|
+
const policy = (0, security_policy_js_1.getDefaultPolicy)("PRODUCTION");
|
|
87
|
+
(0, vitest_1.it)("permite apenas READ", () => {
|
|
88
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_read", policy).allowed).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)("bloqueia tudo que não é READ", () => {
|
|
91
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_write", policy).allowed).toBe(false);
|
|
92
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_create", policy).allowed).toBe(false);
|
|
93
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_delete", policy).allowed).toBe(false);
|
|
94
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_git_stage", policy).allowed).toBe(false);
|
|
95
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkToolAccess)("abap_assign_transport", policy).allowed).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)("mensagem de bloqueio menciona PRODUTIVOS", () => {
|
|
98
|
+
const result = (0, security_policy_js_1.checkToolAccess)("abap_write", policy);
|
|
99
|
+
(0, vitest_1.expect)(result.reason).toContain("PRODUTIVO");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
(0, vitest_1.describe)("checkTransportRequired", () => {
|
|
104
|
+
(0, vitest_1.it)("em DEV sem require_transport, não exige", () => {
|
|
105
|
+
const policy = (0, security_policy_js_1.getDefaultPolicy)("DEVELOPMENT");
|
|
106
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkTransportRequired)("abap_write", undefined, policy).allowed).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)("em QAS com require_transport, exige para WRITE", () => {
|
|
109
|
+
const policy = (0, security_policy_js_1.getDefaultPolicy)("QUALITY");
|
|
110
|
+
const result = (0, security_policy_js_1.checkTransportRequired)("abap_write", undefined, policy);
|
|
111
|
+
(0, vitest_1.expect)(result.allowed).toBe(false);
|
|
112
|
+
(0, vitest_1.expect)(result.reason).toContain("transport request");
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)("em QAS com transport fornecido, permite", () => {
|
|
115
|
+
const policy = (0, security_policy_js_1.getDefaultPolicy)("QUALITY");
|
|
116
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkTransportRequired)("abap_write", "DEVK900123", policy).allowed).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
(0, vitest_1.it)("READ nunca exige transport", () => {
|
|
119
|
+
const policy = (0, security_policy_js_1.getDefaultPolicy)("PRODUCTION");
|
|
120
|
+
(0, vitest_1.expect)((0, security_policy_js_1.checkTransportRequired)("abap_read", undefined, policy).allowed).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
(0, vitest_1.describe)("loadPolicy — validação Zod", () => {
|
|
124
|
+
const originalEnv = { ...process.env };
|
|
125
|
+
(0, vitest_1.afterEach)(() => {
|
|
126
|
+
// Restaurar env
|
|
127
|
+
delete process.env.ABAP_AI_SECURITY_POLICY;
|
|
128
|
+
delete process.env.ABAP_AI_ENV_ROLE;
|
|
129
|
+
});
|
|
130
|
+
(0, vitest_1.it)("aceita policy válida via env var", () => {
|
|
131
|
+
process.env.ABAP_AI_SECURITY_POLICY = JSON.stringify({
|
|
132
|
+
environment_role: "PRODUCTION",
|
|
133
|
+
});
|
|
134
|
+
const policy = (0, security_policy_js_1.loadPolicy)();
|
|
135
|
+
(0, vitest_1.expect)(policy.environment_role).toBe("PRODUCTION");
|
|
136
|
+
});
|
|
137
|
+
(0, vitest_1.it)("rejeita environment_role com typo", () => {
|
|
138
|
+
process.env.ABAP_AI_SECURITY_POLICY = JSON.stringify({
|
|
139
|
+
environment_role: "PRODUCTON",
|
|
140
|
+
});
|
|
141
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.loadPolicy)()).toThrow("[SECURITY]");
|
|
142
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.loadPolicy)()).toThrow("environment_role");
|
|
143
|
+
});
|
|
144
|
+
(0, vitest_1.it)("rejeita campo desconhecido (strict mode)", () => {
|
|
145
|
+
process.env.ABAP_AI_SECURITY_POLICY = JSON.stringify({
|
|
146
|
+
environment_role: "DEVELOPMENT",
|
|
147
|
+
campo_invalido: true,
|
|
148
|
+
});
|
|
149
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.loadPolicy)()).toThrow("[SECURITY]");
|
|
150
|
+
});
|
|
151
|
+
(0, vitest_1.it)("rejeita max_preview_rows negativo", () => {
|
|
152
|
+
process.env.ABAP_AI_SECURITY_POLICY = JSON.stringify({
|
|
153
|
+
max_preview_rows: -5,
|
|
154
|
+
});
|
|
155
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.loadPolicy)()).toThrow("[SECURITY]");
|
|
156
|
+
});
|
|
157
|
+
(0, vitest_1.it)("rejeita allowed_levels com valor inválido", () => {
|
|
158
|
+
process.env.ABAP_AI_SECURITY_POLICY = JSON.stringify({
|
|
159
|
+
allowed_levels: ["READ", "SUPER_ADMIN"],
|
|
160
|
+
});
|
|
161
|
+
(0, vitest_1.expect)(() => (0, security_policy_js_1.loadPolicy)()).toThrow("[SECURITY]");
|
|
162
|
+
});
|
|
163
|
+
(0, vitest_1.it)("usa default DEVELOPMENT quando nenhuma config existe", () => {
|
|
164
|
+
const policy = (0, security_policy_js_1.loadPolicy)();
|
|
165
|
+
(0, vitest_1.expect)(policy.environment_role).toBe("DEVELOPMENT");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool handler wrapper — elimina boilerplate de try/catch + securityGuard
|
|
4
|
+
* em todas as 55 tool registrations.
|
|
5
|
+
*
|
|
6
|
+
* Uso:
|
|
7
|
+
* server.tool("abap_read", description, schema, toolHandler("abap_read", async (args) => {
|
|
8
|
+
* return await abapRead(args);
|
|
9
|
+
* }));
|
|
10
|
+
*
|
|
11
|
+
* O wrapper automaticamente:
|
|
12
|
+
* 1. Chama securityGuard() e retorna erro se bloqueado
|
|
13
|
+
* 2. Executa o handler
|
|
14
|
+
* 3. Wrapa o resultado em { content: [{ type: "text", text }] }
|
|
15
|
+
* 4. Captura erros e retorna mensagem amigável
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.securityGuard = securityGuard;
|
|
19
|
+
exports.toolHandler = toolHandler;
|
|
20
|
+
const security_policy_js_1 = require("./security-policy.js");
|
|
21
|
+
const security_audit_js_1 = require("./security-audit.js");
|
|
22
|
+
/**
|
|
23
|
+
* Verifica se uma tool pode ser executada sob a security policy atual.
|
|
24
|
+
* Retorna null se permitido, ou uma resposta de erro MCP se bloqueado.
|
|
25
|
+
*/
|
|
26
|
+
function securityGuard(toolName, transportRequest) {
|
|
27
|
+
const policy = (0, security_policy_js_1.getPolicy)();
|
|
28
|
+
const riskLevel = (0, security_policy_js_1.getToolRisk)(toolName);
|
|
29
|
+
const accessCheck = (0, security_policy_js_1.checkToolAccess)(toolName, policy);
|
|
30
|
+
if (!accessCheck.allowed) {
|
|
31
|
+
(0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, accessCheck.reason || "policy");
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: accessCheck.reason || `Tool ${toolName} bloqueada.` }],
|
|
34
|
+
isError: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const transportCheck = (0, security_policy_js_1.checkTransportRequired)(toolName, transportRequest, policy);
|
|
38
|
+
if (!transportCheck.allowed) {
|
|
39
|
+
(0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, transportCheck.reason || "transport required");
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: transportCheck.reason || `Transport request obrigatório.` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
(0, security_audit_js_1.auditAllowed)(toolName, riskLevel, policy.environment_role);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Wrapper para tool handlers.
|
|
50
|
+
*
|
|
51
|
+
* @param toolName — nome da tool para securityGuard
|
|
52
|
+
* @param fn — handler que recebe args e retorna string (resultado) ou McpResult
|
|
53
|
+
* @param opts — opções: transportField = nome do campo com transport_request nos args
|
|
54
|
+
*/
|
|
55
|
+
function toolHandler(toolName, fn, opts) {
|
|
56
|
+
return async (args) => {
|
|
57
|
+
// Security guard
|
|
58
|
+
const transportRequest = opts?.transportField
|
|
59
|
+
? args[opts.transportField]
|
|
60
|
+
: undefined;
|
|
61
|
+
const blocked = securityGuard(toolName, transportRequest);
|
|
62
|
+
if (blocked)
|
|
63
|
+
return blocked;
|
|
64
|
+
// Execute handler
|
|
65
|
+
try {
|
|
66
|
+
const result = await fn(args);
|
|
67
|
+
if (typeof result === "string") {
|
|
68
|
+
return { content: [{ type: "text", text: result }] };
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: `Erro em ${toolName}: ${message}` }],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
package/dist/tools/activate.js
CHANGED
|
@@ -7,11 +7,33 @@ function parseActivationResponse(xml) {
|
|
|
7
7
|
const executed = /activationExecuted="true"/.test(xml);
|
|
8
8
|
const checked = /checkExecuted="true"/.test(xml);
|
|
9
9
|
const messages = [];
|
|
10
|
+
// Formato 1: <chkl:message type="E"><chkl:shortText>...</chkl:shortText>
|
|
10
11
|
const msgRegex = /<chkl:message[^>]*type="([^"]*)"[^>]*>[\s\S]*?<chkl:shortText[^>]*>([\s\S]*?)<\/chkl:shortText>[\s\S]*?(?:<chkl:uri[^>]*line="(\d+)")?/g;
|
|
11
12
|
let match;
|
|
12
13
|
while ((match = msgRegex.exec(xml)) !== null) {
|
|
13
14
|
messages.push({ type: match[1], text: match[2].trim(), line: match[3] });
|
|
14
15
|
}
|
|
16
|
+
// Formato 2: <msg type="E" line="1"><shortText><txt>...</txt></shortText></msg>
|
|
17
|
+
if (messages.length === 0) {
|
|
18
|
+
const msgAltRegex = /<msg[^>]*type="([^"]*)"[^>]*line="(\d+)"[^>]*>[\s\S]*?<txt>([^<]+)<\/txt>/g;
|
|
19
|
+
while ((match = msgAltRegex.exec(xml)) !== null) {
|
|
20
|
+
messages.push({ type: match[1], text: match[3].trim(), line: match[2] });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Formato 3: <message>...</message> (exception genérica SAP)
|
|
24
|
+
if (messages.length === 0) {
|
|
25
|
+
const excRegex = /<(?:exc:)?message[^>]*>([^<]+)<\/(?:exc:)?message>/gi;
|
|
26
|
+
while ((match = excRegex.exec(xml)) !== null) {
|
|
27
|
+
messages.push({ type: "E", text: match[1].trim() });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Formato 4: <shortText>...</shortText> sem <msg> wrapper
|
|
31
|
+
if (messages.length === 0) {
|
|
32
|
+
const shortRegex = /<shortText>(?:<txt>)?([^<]+)(?:<\/txt>)?<\/shortText>/gi;
|
|
33
|
+
while ((match = shortRegex.exec(xml)) !== null) {
|
|
34
|
+
messages.push({ type: "E", text: match[1].trim() });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
// Se não executou check nem ativação e não há erros → objeto já estava ativo
|
|
16
38
|
const alreadyActive = !executed && !checked && messages.length === 0;
|
|
17
39
|
return { success: executed || alreadyActive, alreadyActive, messages };
|
|
@@ -32,14 +54,15 @@ async function abapActivate(input) {
|
|
|
32
54
|
},
|
|
33
55
|
params: { method: "activate", preauditRequested: "true" },
|
|
34
56
|
responseType: "text",
|
|
35
|
-
// Aceita
|
|
36
|
-
|
|
37
|
-
validateStatus: (status) => status === 200 || status === 400,
|
|
57
|
+
// Aceita qualquer status < 500 para parsear mensagens de erro do SAP
|
|
58
|
+
validateStatus: (status) => status < 500,
|
|
38
59
|
});
|
|
39
60
|
const { success, alreadyActive, messages } = parseActivationResponse(response.data);
|
|
40
61
|
if (!success) {
|
|
41
62
|
const errors = messages.map((m) => `[${m.type}]${m.line ? ` linha ${m.line}` : ""}: ${m.text}`).join("\n");
|
|
42
|
-
|
|
63
|
+
const rawBody = typeof response.data === "string" ? response.data.substring(0, 500) : "";
|
|
64
|
+
const fallback = rawBody ? `\nResposta SAP (HTTP ${response.status}):\n${rawBody}` : "";
|
|
65
|
+
throw new Error(`Ativação falhou:\n${errors || `Erro desconhecido (HTTP ${response.status})${fallback}`}`);
|
|
43
66
|
}
|
|
44
67
|
if (alreadyActive) {
|
|
45
68
|
return `Objeto ${object_name.toUpperCase()} já estava ativo — nenhuma ação necessária.`;
|
package/dist/tools/create.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.abapCreateTool = void 0;
|
|
4
|
+
exports.buildPayload = buildPayload;
|
|
4
5
|
exports.abapCreate = abapCreate;
|
|
5
6
|
const adt_client_js_1 = require("../adt-client.js");
|
|
6
7
|
// Content-Types para criação — S/4HANA (v2/v3) com fallback para ECC (v1)
|
|
@@ -24,6 +25,7 @@ const CONTENT_TYPE = {
|
|
|
24
25
|
const PROGRAM_TYPE = {
|
|
25
26
|
"PROG/P": "executableProgram",
|
|
26
27
|
};
|
|
28
|
+
/** @internal Exported for testing */
|
|
27
29
|
function buildPayload(input) {
|
|
28
30
|
const { object_type, object_name, description, package: pkg = "$TMP", srvd_name, binding_type = "ODATA;V4;UI" } = input;
|
|
29
31
|
const name = object_name.toUpperCase();
|
package/dist/tools/delete.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* abap_delete — BLOQUEADO permanentemente.
|
|
4
|
+
*
|
|
5
|
+
* Decisão de produto: exclusão de objetos ABAP não é suportada pelo LKPABAP.ia.
|
|
6
|
+
* Operação destrutiva demais para ser executada via IA.
|
|
7
|
+
* Use SE80 (SAP GUI) ou Eclipse ADT para excluir objetos.
|
|
8
|
+
*
|
|
9
|
+
* Este arquivo é mantido apenas para evitar erros de import em código legado.
|
|
10
|
+
*/
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
12
|
exports.abapDeleteObject = abapDeleteObject;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const name = object_name.toUpperCase();
|
|
8
|
-
const adtPath = (0, adt_client_js_1.resolveAdtPath)(object_type);
|
|
9
|
-
const objectPath = `/${adtPath}/${name}`;
|
|
10
|
-
await (0, adt_client_js_1.adtLockAndDelete)(objectPath, transport_request);
|
|
11
|
-
return `Objeto ${name} (${object_type}) excluído com sucesso.`;
|
|
13
|
+
async function abapDeleteObject() {
|
|
14
|
+
throw new Error("abap_delete está permanentemente bloqueado no LKPABAP.ia. "
|
|
15
|
+
+ "Use SE80 no SAP GUI ou Eclipse ADT para excluir objetos.");
|
|
12
16
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* abap_execute — executa um programa ABAP via ICF handler do Security Agent.
|
|
4
|
+
*
|
|
5
|
+
* Chama POST /sap/z/lkpai/guard/exec com o nome do programa.
|
|
6
|
+
* O handler ABAP faz SUBMIT ... EXPORTING LIST TO MEMORY, captura o output
|
|
7
|
+
* e retorna como texto.
|
|
8
|
+
*
|
|
9
|
+
* Requer: ICF handler /sap/z/lkpai/guard ativado no SICF.
|
|
10
|
+
* Restrição: apenas programas Z* e Y* (namespace cliente).
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.abapExecute = abapExecute;
|
|
47
|
+
const adt_client_js_1 = require("../adt-client.js");
|
|
48
|
+
const SAP_URL = process.env.SAP_URL ?? "";
|
|
49
|
+
const SAP_USER = process.env.SAP_USER ?? "";
|
|
50
|
+
const SAP_PASS = process.env.SAP_PASS ?? "";
|
|
51
|
+
const SAP_CLIENT = process.env.SAP_CLIENT ?? "100";
|
|
52
|
+
async function abapExecute(input) {
|
|
53
|
+
const { program_name, variant } = input;
|
|
54
|
+
// Garantir sessão ADT ativa (para CSRF se necessário)
|
|
55
|
+
await (0, adt_client_js_1.ensureSession)();
|
|
56
|
+
const nativeHttps = await Promise.resolve().then(() => __importStar(require("https")));
|
|
57
|
+
const nativeHttp = await Promise.resolve().then(() => __importStar(require("http")));
|
|
58
|
+
const url = new URL(`${SAP_URL}/sap/z/lkpai/guard/exec`);
|
|
59
|
+
url.searchParams.set("sap-client", SAP_CLIENT);
|
|
60
|
+
const body = JSON.stringify({
|
|
61
|
+
program: program_name.toUpperCase(),
|
|
62
|
+
variant: variant || "",
|
|
63
|
+
});
|
|
64
|
+
const transport = url.protocol === "https:" ? nativeHttps : nativeHttp;
|
|
65
|
+
const response = await new Promise((resolve, reject) => {
|
|
66
|
+
const req = transport.request({
|
|
67
|
+
hostname: url.hostname,
|
|
68
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
69
|
+
path: url.pathname + url.search,
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: "Basic " + Buffer.from(`${SAP_USER}:${SAP_PASS}`).toString("base64"),
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
Accept: "application/json",
|
|
75
|
+
},
|
|
76
|
+
rejectUnauthorized: false,
|
|
77
|
+
timeout: 60000, // programas podem demorar
|
|
78
|
+
}, (res) => {
|
|
79
|
+
let data = "";
|
|
80
|
+
res.on("data", (chunk) => (data += chunk));
|
|
81
|
+
res.on("end", () => resolve({ status: res.statusCode || 0, body: data }));
|
|
82
|
+
});
|
|
83
|
+
req.on("error", reject);
|
|
84
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("Timeout (60s)")); });
|
|
85
|
+
req.write(body);
|
|
86
|
+
req.end();
|
|
87
|
+
});
|
|
88
|
+
if (response.status === 404) {
|
|
89
|
+
throw new Error("ICF handler /sap/z/lkpai/guard não está ativado neste sistema.\n"
|
|
90
|
+
+ "Peça ao BASIS para ativar o serviço no SICF:\n"
|
|
91
|
+
+ " SICF → /sap/z/lkpai/guard → Ativar\n"
|
|
92
|
+
+ " Handler: ZCL_LKP_AI_ICF_HANDLER");
|
|
93
|
+
}
|
|
94
|
+
// Parsear resposta JSON
|
|
95
|
+
try {
|
|
96
|
+
const result = JSON.parse(response.body);
|
|
97
|
+
if (result.error) {
|
|
98
|
+
throw new Error(result.error);
|
|
99
|
+
}
|
|
100
|
+
if (result.success) {
|
|
101
|
+
const output = (result.output || "").replace(/\\n/g, "\n");
|
|
102
|
+
return `Programa ${result.program} executado com sucesso.\n\n--- Output ---\n${output}`;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Resposta inesperada (HTTP ${response.status}): ${response.body.substring(0, 300)}`);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (e instanceof SyntaxError) {
|
|
108
|
+
throw new Error(`Resposta não-JSON do SAP (HTTP ${response.status}): ${response.body.substring(0, 300)}`);
|
|
109
|
+
}
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/tools/transports.js
CHANGED
|
@@ -43,7 +43,8 @@ async function abapListTransports(input) {
|
|
|
43
43
|
async function abapAssignTransport(input) {
|
|
44
44
|
const { transport_request, object_type, object_name } = input;
|
|
45
45
|
const adtPath = (0, adt_client_js_1.resolveAdtPath)(object_type);
|
|
46
|
-
const
|
|
46
|
+
const objectPath = `/${adtPath}/${object_name.toUpperCase()}`; // relativo ao baseURL (para http.post)
|
|
47
|
+
const objectUri = `/sap/bc/adt/${adtPath}/${object_name.toUpperCase()}`; // absoluto (para XML refs)
|
|
47
48
|
const csrf = await (0, adt_client_js_1.ensureSession)();
|
|
48
49
|
// Endpoint correto: POST /cts/transportchecks com o URI do objeto
|
|
49
50
|
// Retorna os transportes candidatos. Se transport_request é fornecido,
|
|
@@ -75,14 +76,14 @@ async function abapAssignTransport(input) {
|
|
|
75
76
|
// Fallback: se transportchecks não aceita corrNr, tentar via lock com corrNr
|
|
76
77
|
// (padrão ADT — lock do objeto atribui ao transporte automaticamente)
|
|
77
78
|
if (response.status === 404 || response.status === 400) {
|
|
78
|
-
const lockResp = await adt_client_js_1.http.post(
|
|
79
|
+
const lockResp = await adt_client_js_1.http.post(objectPath, null, {
|
|
79
80
|
headers: { "X-CSRF-Token": csrf },
|
|
80
81
|
params: { _action: "LOCK", accessMode: "MODIFY", corrNr: transport_request },
|
|
81
82
|
validateStatus: (s) => s >= 200 && s < 500,
|
|
82
83
|
});
|
|
83
84
|
if (lockResp.status >= 200 && lockResp.status < 300) {
|
|
84
85
|
// Unlock imediatamente — o lock já atribuiu o objeto ao transporte
|
|
85
|
-
await adt_client_js_1.http.post(
|
|
86
|
+
await adt_client_js_1.http.post(objectPath, null, {
|
|
86
87
|
headers: { "X-CSRF-Token": csrf },
|
|
87
88
|
params: { _action: "UNLOCK" },
|
|
88
89
|
validateStatus: () => true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkup-ai/abap-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"copilot"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsc && rm -rf dist/knowledge && cp -r src/knowledge dist/knowledge",
|
|
31
|
+
"build": "tsc && rm -rf dist/knowledge && cp -r src/knowledge dist/knowledge && rm -rf dist/abap && cp -r src/abap dist/abap",
|
|
32
32
|
"start": "node dist/index.js",
|
|
33
33
|
"dev": "tsc --watch",
|
|
34
34
|
"prepublishOnly": "npm run build",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"release:patch": "npm version patch && npm publish",
|
|
37
37
|
"release:minor": "npm version minor && npm publish",
|
|
38
38
|
"release:next": "npm version prerelease --preid=next && npm publish --tag next",
|
|
39
|
-
"promote:next": "npm dist-tag add @linkup-ai/abap-ai@$(node -p \"require('./package.json').version\") latest"
|
|
39
|
+
"promote:next": "npm dist-tag add @linkup-ai/abap-ai@$(node -p \"require('./package.json').version\") latest",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
44
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"@types/node": "^22.0.0",
|
|
50
52
|
"@types/prompts": "^2.4.9",
|
|
51
53
|
"@types/tough-cookie": "^4.0.5",
|
|
52
|
-
"typescript": "^5.7.0"
|
|
54
|
+
"typescript": "^5.7.0",
|
|
55
|
+
"vitest": "^3.2.4"
|
|
53
56
|
}
|
|
54
57
|
}
|