agent-master-devsecops 3.9.0 → 5.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.
Files changed (82) hide show
  1. package/README.md +205 -371
  2. package/agents/AGENT_DEVSECOPS.md +32 -417
  3. package/agents/AGENT_INFRA.md +31 -0
  4. package/agents/AGENT_ORCHESTRATOR.md +39 -264
  5. package/agents/AGENT_QA.md +58 -186
  6. package/agents/AGENT_SCRUM.md +23 -190
  7. package/agents/AGENT_SRE.md +22 -173
  8. package/agents/AGENT_UIUX.md +26 -162
  9. package/docs/ARCHITECTURE.md +152 -148
  10. package/docs/CALCULATIONS.md +17 -17
  11. package/docs/CHANGELOG.md +112 -49
  12. package/docs/ENGINEERING_RULES.md +56 -252
  13. package/docs/IDEAS.md +17 -17
  14. package/docs/LOCAL_BACKLOG.md +20 -0
  15. package/docs/STATE.md +111 -43
  16. package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -58
  17. package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -58
  18. package/package.json +59 -47
  19. package/scripts/auditors/a11y_auditor.js +462 -0
  20. package/scripts/auditors/code_quality_auditor.js +444 -0
  21. package/scripts/auditors/contract_test_validator.js +396 -0
  22. package/scripts/auditors/eval_harness.js +213 -0
  23. package/scripts/auditors/git_hygiene_linter.js +391 -0
  24. package/scripts/auditors/pr_reviewer.js +627 -0
  25. package/scripts/auditors/sast_scanner.js +442 -0
  26. package/scripts/auditors/sca_dependency_auditor.js +673 -0
  27. package/scripts/{bootstrap_cli.js → bin/bootstrap_cli.js} +597 -468
  28. package/scripts/bin/domain_idea_miner.js +658 -0
  29. package/scripts/bin/smart_onboarding.js +905 -0
  30. package/scripts/{web_inspector.js → bin/web_inspector.js} +578 -355
  31. package/scripts/core/architecture_advisor.js +378 -0
  32. package/scripts/core/ast_skeletonizer.js +413 -0
  33. package/scripts/core/autonomous_remediation_engine.js +305 -0
  34. package/scripts/{circuit_breaker.js → core/circuit_breaker.js} +546 -330
  35. package/scripts/core/context_compressor.js +237 -0
  36. package/scripts/core/fast_feature_scaffold.js +840 -0
  37. package/scripts/core/hot_state_cache.js +238 -0
  38. package/scripts/core/knowledge_gateway.js +888 -0
  39. package/scripts/{magic_prompt_generator.js → core/magic_prompt_generator.js} +250 -205
  40. package/scripts/{multi_agent_orchestrator.js → core/multi_agent_orchestrator.js} +421 -258
  41. package/scripts/{otel_tracer.js → core/otel_tracer.js} +335 -228
  42. package/scripts/{perf_benchmark.js → core/perf_benchmark.js} +212 -153
  43. package/scripts/core/sarif_engine.js +299 -0
  44. package/scripts/{sre_remediation_engine.js → core/sre_remediation_engine.js} +236 -174
  45. package/scripts/{universal_packager.js → core/universal_packager.js} +357 -313
  46. package/scripts/integrations/atlassian_client.js +600 -0
  47. package/scripts/integrations/atlassian_sync_action.js +576 -0
  48. package/scripts/integrations/confluence_sync.js +684 -0
  49. package/scripts/integrations/credentials_guard.js +500 -0
  50. package/scripts/integrations/jira_manager.js +722 -0
  51. package/scripts/mcp/handlers/atlassian_handlers.js +300 -0
  52. package/scripts/mcp/handlers/core_handlers.js +419 -0
  53. package/scripts/mcp/handlers/qa_handlers.js +502 -0
  54. package/scripts/mcp/handlers/rules_handlers.js +379 -0
  55. package/scripts/mcp/handlers/scaffold_handlers.js +334 -0
  56. package/scripts/mcp/handlers/sre_handlers.js +443 -0
  57. package/scripts/mcp/resources_registry.js +453 -0
  58. package/scripts/mcp/server.js +408 -0
  59. package/scripts/mcp/tool_handlers.js +444 -0
  60. package/scripts/mcp/tools_registry.js +1123 -0
  61. package/scripts/mcp_server.js +236 -1884
  62. package/.cursorignore +0 -23
  63. package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +0 -53
  64. package/scripts/a11y_auditor.js +0 -187
  65. package/scripts/ast_skeletonizer.js +0 -220
  66. package/scripts/atlassian_client.js +0 -238
  67. package/scripts/atlassian_sync_action.js +0 -269
  68. package/scripts/code_quality_auditor.js +0 -142
  69. package/scripts/confluence_sync.js +0 -337
  70. package/scripts/context_compressor.js +0 -139
  71. package/scripts/contract_test_validator.js +0 -166
  72. package/scripts/credentials_guard.js +0 -216
  73. package/scripts/domain_idea_miner.js +0 -364
  74. package/scripts/eval_harness.js +0 -100
  75. package/scripts/fast_feature_scaffold.js +0 -338
  76. package/scripts/git_hygiene_linter.js +0 -186
  77. package/scripts/hot_state_cache.js +0 -95
  78. package/scripts/jira_manager.js +0 -408
  79. package/scripts/pr_reviewer.js +0 -373
  80. package/scripts/sast_scanner.js +0 -180
  81. package/scripts/sca_dependency_auditor.js +0 -167
  82. package/scripts/smart_onboarding.js +0 -610
@@ -1,330 +1,546 @@
1
- #!/usr/bin/env node
2
- /**
3
- * scripts/circuit_breaker.js
4
- *
5
- * Circuit Breaker Inteligente Multi-Provedor para LLMs (AGDEV-9 / I-10).
6
- * Abstração de resiliência e auto-cura (CLOSED, OPEN, HALF_OPEN) com fallback automático
7
- * entre provedores de IA (Anthropic, OpenAI, Groq, Ollama) diante de erros HTTP 429/5xx ou timeouts.
8
- */
9
-
10
- const STATES = {
11
- CLOSED: 'CLOSED', // Operação normal
12
- OPEN: 'OPEN', // Circuito rompido; chamadas desviadas para fallback
13
- HALF_OPEN: 'HALF_OPEN' // Sonda de teste para verificar recuperação
14
- };
15
-
16
- class CircuitBreaker {
17
- constructor(name, options = {}) {
18
- this.name = name;
19
- this.failureThreshold = options.failureThreshold || 3;
20
- this.recoveryTimeoutMs = options.recoveryTimeoutMs || 30000;
21
- this.state = STATES.CLOSED;
22
- this.failureCount = 0;
23
- this.successCount = 0;
24
- this.lastFailureTime = null;
25
- this.lastStateChange = Date.now();
26
- }
27
-
28
- isOpen() {
29
- if (this.state === STATES.OPEN) {
30
- const now = Date.now();
31
- if (now - this.lastFailureTime >= this.recoveryTimeoutMs) {
32
- this.state = STATES.HALF_OPEN;
33
- this.lastStateChange = now;
34
- return false;
35
- }
36
- return true;
37
- }
38
- return false;
39
- }
40
-
41
- recordSuccess() {
42
- this.failureCount = 0;
43
- this.successCount++;
44
- if (this.state === STATES.HALF_OPEN) {
45
- this.state = STATES.CLOSED;
46
- this.lastStateChange = Date.now();
47
- }
48
- }
49
-
50
- recordFailure(error) {
51
- this.failureCount++;
52
- this.lastFailureTime = Date.now();
53
-
54
- if (this.state === STATES.HALF_OPEN || this.failureCount >= this.failureThreshold) {
55
- this.state = STATES.OPEN;
56
- this.lastStateChange = Date.now();
57
- }
58
- }
59
-
60
- async execute(primaryFn, fallbackFn = null) {
61
- if (this.isOpen()) {
62
- if (fallbackFn) {
63
- return {
64
- result: await fallbackFn(new Error(`Circuito ${this.name} está ABERTO (OPEN). Redirecionando para fallback.`)),
65
- circuit: this.name,
66
- state: this.state,
67
- fallbackExecuted: true
68
- };
69
- }
70
- throw new Error(`Circuito ${this.name} está ABERTO (OPEN). Nenhuma chamada permitida.`);
71
- }
72
-
73
- try {
74
- const result = await primaryFn();
75
- this.recordSuccess();
76
- return {
77
- result,
78
- circuit: this.name,
79
- state: this.state,
80
- fallbackExecuted: false
81
- };
82
- } catch (err) {
83
- this.recordFailure(err);
84
- if (fallbackFn) {
85
- return {
86
- result: await fallbackFn(err),
87
- circuit: this.name,
88
- state: this.state,
89
- fallbackExecuted: true,
90
- error: err.message
91
- };
92
- }
93
- throw err;
94
- }
95
- }
96
-
97
- executeSync(primaryFn, fallbackFn = null) {
98
- if (this.isOpen()) {
99
- if (fallbackFn) {
100
- return {
101
- result: fallbackFn(new Error(`Circuito ${this.name} está ABERTO (OPEN). Redirecionando para fallback.`)),
102
- circuit: this.name,
103
- state: this.state,
104
- fallbackExecuted: true
105
- };
106
- }
107
- throw new Error(`Circuito ${this.name} está ABERTO (OPEN). Nenhuma chamada permitida.`);
108
- }
109
-
110
- try {
111
- const result = primaryFn();
112
- this.recordSuccess();
113
- return {
114
- result,
115
- circuit: this.name,
116
- state: this.state,
117
- fallbackExecuted: false
118
- };
119
- } catch (err) {
120
- this.recordFailure(err);
121
- if (fallbackFn) {
122
- return {
123
- result: fallbackFn(err),
124
- circuit: this.name,
125
- state: this.state,
126
- fallbackExecuted: true,
127
- error: err.message
128
- };
129
- }
130
- throw err;
131
- }
132
- }
133
-
134
- reset() {
135
- this.state = STATES.CLOSED;
136
- this.failureCount = 0;
137
- this.successCount = 0;
138
- this.lastFailureTime = null;
139
- this.lastStateChange = Date.now();
140
- }
141
-
142
- getStatus() {
143
- return {
144
- name: this.name,
145
- state: this.state,
146
- failureCount: this.failureCount,
147
- failureThreshold: this.failureThreshold,
148
- recoveryTimeoutMs: this.recoveryTimeoutMs,
149
- isOpen: this.isOpen()
150
- };
151
- }
152
- }
153
-
154
- class MultiProviderRouter {
155
- constructor(providers = ['anthropic', 'openai', 'groq', 'ollama'], options = {}) {
156
- this.circuits = new Map();
157
- providers.forEach(p => {
158
- this.circuits.set(p, new CircuitBreaker(p, options));
159
- });
160
- this.providerChain = providers;
161
- }
162
-
163
- async dispatch(taskFnMap) {
164
- const attempted = [];
165
-
166
- for (const provider of this.providerChain) {
167
- const circuit = this.circuits.get(provider);
168
- attempted.push(provider);
169
-
170
- if (!circuit.isOpen()) {
171
- const fn = taskFnMap[provider] || taskFnMap['default'];
172
- if (!fn) continue;
173
-
174
- try {
175
- const res = await circuit.execute(fn);
176
- return {
177
- success: true,
178
- providerUsed: provider,
179
- attempted,
180
- result: res.result,
181
- circuitStates: this.getAllStatuses()
182
- };
183
- } catch (err) {
184
- // Erro registrado pelo circuit breaker; continua para o próximo provedor na cadeia
185
- continue;
186
- }
187
- }
188
- }
189
-
190
- return {
191
- success: false,
192
- error: 'Todos os provedores de IA estão indisponíveis ou com circuito ABERTO.',
193
- attempted,
194
- circuitStates: this.getAllStatuses()
195
- };
196
- }
197
-
198
- dispatchSync(taskFnMap) {
199
- const attempted = [];
200
-
201
- for (const provider of this.providerChain) {
202
- const circuit = this.circuits.get(provider);
203
- attempted.push(provider);
204
-
205
- if (!circuit.isOpen()) {
206
- const fn = taskFnMap[provider] || taskFnMap['default'];
207
- if (!fn) continue;
208
-
209
- try {
210
- const res = circuit.executeSync(fn);
211
- return {
212
- success: true,
213
- providerUsed: provider,
214
- attempted,
215
- result: res.result,
216
- circuitStates: this.getAllStatuses()
217
- };
218
- } catch (err) {
219
- continue;
220
- }
221
- }
222
- }
223
-
224
- return {
225
- success: false,
226
- error: 'Todos os provedores de IA estão indisponíveis ou com circuito ABERTO.',
227
- attempted,
228
- circuitStates: this.getAllStatuses()
229
- };
230
- }
231
-
232
- getAllStatuses() {
233
- const statuses = {};
234
- for (const [name, circuit] of this.circuits.entries()) {
235
- statuses[name] = circuit.getStatus();
236
- }
237
- return statuses;
238
- }
239
-
240
- resetAll() {
241
- for (const circuit of this.circuits.values()) {
242
- circuit.reset();
243
- }
244
- }
245
- }
246
-
247
- async function runCli() {
248
- console.log('=== Simulação de Resiliência Multi-Provedor com Circuit Breaker (I-10) ===');
249
- const router = new MultiProviderRouter(['anthropic', 'openai', 'groq', 'ollama'], { failureThreshold: 2 });
250
-
251
- // Simula indisponibilidade em anthropic e openai
252
- const taskMap = {
253
- anthropic: async () => { throw new Error('HTTP 429: Too Many Requests (Rate limit exceeded)'); },
254
- openai: async () => { throw new Error('HTTP 503: Service Unavailable'); },
255
- groq: async () => { return 'Resposta gerada com sucesso via Groq (Llama 3.3 70B)'; },
256
- ollama: async () => { return 'Resposta local via Ollama'; }
257
- };
258
-
259
- const dispatch1 = await router.dispatch(taskMap);
260
- console.log('\nDespacho 1 (Anthropic & OpenAI falham -> Fallback para Groq):');
261
- console.log(`Provedor Utilizado: ${dispatch1.providerUsed}`);
262
- console.log(`Tentativas: ${dispatch1.attempted.join(' -> ')}`);
263
- console.log(`Resultado: ${dispatch1.result}`);
264
-
265
- console.log('\nStatus dos Circuitos:');
266
- console.log(JSON.stringify(dispatch1.circuitStates, null, 2));
267
- }
268
-
269
- if (require.main === module) {
270
- runCli().catch(console.error);
271
- }
272
-
273
-
274
- class AntiThrashingGuard {
275
- constructor(options = {}) {
276
- this.maxIdenticalFailures = options.maxIdenticalFailures || 2;
277
- this.history = [];
278
- this.thrashingTripped = false;
279
- }
280
-
281
- normalizeError(error) {
282
- if (!error) return 'unknown_error';
283
- const msg = typeof error === 'string' ? error : (error.message || String(error));
284
- return msg
285
- .replace(/:\d+:\d+/g, '')
286
- .replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\b/g, '')
287
- .replace(/0x[0-9a-fA-F]+/g, '')
288
- .trim();
289
- }
290
-
291
- recordAttempt(error = null) {
292
- if (!error) {
293
- this.history = [];
294
- this.thrashingTripped = false;
295
- return { isThrashing: false, count: 0 };
296
- }
297
-
298
- const norm = this.normalizeError(error);
299
- this.history.push({ signature: norm, timestamp: Date.now() });
300
-
301
- if (this.history.length >= this.maxIdenticalFailures) {
302
- const recent = this.history.slice(-this.maxIdenticalFailures);
303
- const allIdentical = recent.every(h => h.signature === norm);
304
- if (allIdentical) {
305
- this.thrashingTripped = true;
306
- return {
307
- isThrashing: true,
308
- count: this.history.length,
309
- signature: norm,
310
- message: `[ANTI-THRASHING FINOPS VETO]: Detectadas ${this.maxIdenticalFailures} falhas consecutivas com erro idêntico ("${norm.substring(0, 80)}..."). Execução autônoma interrompida para evitar queima descontrolada de tokens.`
311
- };
312
- }
313
- }
314
-
315
- return { isThrashing: false, count: this.history.length, signature: norm };
316
- }
317
-
318
- reset() {
319
- this.history = [];
320
- this.thrashingTripped = false;
321
- }
322
- }
323
-
324
-
325
- module.exports = {
326
- STATES,
327
- CircuitBreaker,
328
- MultiProviderRouter,
329
- AntiThrashingGuard
330
- };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/circuit_breaker.js
4
+ *
5
+ * Circuit Breaker Inteligente Multi-Provedor para LLMs (AGDEV-9 / I-10).
6
+ * Abstração de resiliência e auto-cura (CLOSED, OPEN, HALF_OPEN) com fallback automático
7
+ * entre provedores de IA (Anthropic, OpenAI, Groq, Ollama) diante de erros HTTP 429/5xx ou timeouts.
8
+ */
9
+
10
+ const { createSarifReport } = require('./sarif_engine.js');
11
+
12
+ const STATES = {
13
+ CLOSED: 'CLOSED', // Operação normal
14
+ OPEN: 'OPEN', // Circuito rompido; chamadas desviadas para fallback
15
+ HALF_OPEN: 'HALF_OPEN' // Sonda de teste para verificar recuperação
16
+ };
17
+
18
+ class CircuitBreaker {
19
+ constructor(name, options = {}) {
20
+ this.name = name;
21
+ this.failureThreshold = options.failureThreshold || 3;
22
+ this.recoveryTimeoutMs = options.recoveryTimeoutMs || 30000;
23
+ this.state = STATES.CLOSED;
24
+ this.failureCount = 0;
25
+ this.successCount = 0;
26
+ this.lastFailureTime = null;
27
+ this.lastStateChange = Date.now();
28
+ }
29
+
30
+ isOpen() {
31
+ if (this.state === STATES.OPEN) {
32
+ const now = Date.now();
33
+ if (now - this.lastFailureTime >= this.recoveryTimeoutMs) {
34
+ this.state = STATES.HALF_OPEN;
35
+ this.lastStateChange = now;
36
+ return false;
37
+ }
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ recordSuccess() {
44
+ this.failureCount = 0;
45
+ this.successCount++;
46
+ if (this.state === STATES.HALF_OPEN) {
47
+ this.state = STATES.CLOSED;
48
+ this.lastStateChange = Date.now();
49
+ }
50
+ }
51
+
52
+ recordFailure(error) {
53
+ this.failureCount++;
54
+ this.lastFailureTime = Date.now();
55
+
56
+ if (this.state === STATES.HALF_OPEN || this.failureCount >= this.failureThreshold) {
57
+ this.state = STATES.OPEN;
58
+ this.lastStateChange = Date.now();
59
+ }
60
+ }
61
+
62
+ async execute(primaryFn, fallbackFn = null) {
63
+ if (this.isOpen()) {
64
+ if (fallbackFn) {
65
+ return {
66
+ result: await fallbackFn(new Error(`Circuito ${this.name} está ABERTO (OPEN). Redirecionando para fallback.`)),
67
+ circuit: this.name,
68
+ state: this.state,
69
+ fallbackExecuted: true
70
+ };
71
+ }
72
+ throw new Error(`Circuito ${this.name} está ABERTO (OPEN). Nenhuma chamada permitida.`);
73
+ }
74
+
75
+ try {
76
+ const result = await primaryFn();
77
+ this.recordSuccess();
78
+ return {
79
+ result,
80
+ circuit: this.name,
81
+ state: this.state,
82
+ fallbackExecuted: false
83
+ };
84
+ } catch (err) {
85
+ this.recordFailure(err);
86
+ if (fallbackFn) {
87
+ return {
88
+ result: await fallbackFn(err),
89
+ circuit: this.name,
90
+ state: this.state,
91
+ fallbackExecuted: true,
92
+ error: err.message
93
+ };
94
+ }
95
+ throw err;
96
+ }
97
+ }
98
+
99
+ executeSync(primaryFn, fallbackFn = null) {
100
+ if (this.isOpen()) {
101
+ if (fallbackFn) {
102
+ return {
103
+ result: fallbackFn(new Error(`Circuito ${this.name} está ABERTO (OPEN). Redirecionando para fallback.`)),
104
+ circuit: this.name,
105
+ state: this.state,
106
+ fallbackExecuted: true
107
+ };
108
+ }
109
+ throw new Error(`Circuito ${this.name} está ABERTO (OPEN). Nenhuma chamada permitida.`);
110
+ }
111
+
112
+ try {
113
+ const result = primaryFn();
114
+ this.recordSuccess();
115
+ return {
116
+ result,
117
+ circuit: this.name,
118
+ state: this.state,
119
+ fallbackExecuted: false
120
+ };
121
+ } catch (err) {
122
+ this.recordFailure(err);
123
+ if (fallbackFn) {
124
+ return {
125
+ result: fallbackFn(err),
126
+ circuit: this.name,
127
+ state: this.state,
128
+ fallbackExecuted: true,
129
+ error: err.message
130
+ };
131
+ }
132
+ throw err;
133
+ }
134
+ }
135
+
136
+ reset() {
137
+ this.state = STATES.CLOSED;
138
+ this.failureCount = 0;
139
+ this.successCount = 0;
140
+ this.lastFailureTime = null;
141
+ this.lastStateChange = Date.now();
142
+ }
143
+
144
+ getStatus() {
145
+ return {
146
+ name: this.name,
147
+ state: this.state,
148
+ failureCount: this.failureCount,
149
+ failureThreshold: this.failureThreshold,
150
+ recoveryTimeoutMs: this.recoveryTimeoutMs,
151
+ isOpen: this.isOpen()
152
+ };
153
+ }
154
+
155
+ auditToSarif() {
156
+ const status = this.getStatus();
157
+ const findings = [];
158
+
159
+ if (status.isOpen) {
160
+ findings.push({
161
+ ruleId: 'RULE-CIRCUIT-OPEN',
162
+ name: 'CircuitBreakerTripped',
163
+ severity: 'HIGH',
164
+ message: `O circuito ${status.name} está ABERTO após ${status.failureCount} falhas consecutivas.`,
165
+ remediation: 'Verifique a saúde do provedor ou acione o failover automático.'
166
+ });
167
+ }
168
+
169
+ return createSarifReport({
170
+ toolName: `agent-master-circuit-breaker-${status.name}`,
171
+ rules: [
172
+ {
173
+ id: 'RULE-CIRCUIT-OPEN',
174
+ name: 'CircuitBreakerTripped',
175
+ description: 'Identifica quando um provedor de IA entrou em estado de circuito rompido',
176
+ severity: 'HIGH',
177
+ cwe: 'CWE-400'
178
+ }
179
+ ],
180
+ findings,
181
+ properties: {
182
+ circuitName: status.name,
183
+ state: status.state,
184
+ failureCount: status.failureCount
185
+ }
186
+ });
187
+ }
188
+
189
+ formatReport() {
190
+ const s = this.getStatus();
191
+ return [
192
+ `### Status do Circuito [${s.name}]`,
193
+ `- **Estado:** \`${s.state}\``,
194
+ `- **Falhas:** ${s.failureCount} / ${s.failureThreshold}`,
195
+ `- **Tempo de Recuperação:** ${s.recoveryTimeoutMs}ms`,
196
+ `- **Aberto (Bloqueado):** ${s.isOpen ? 'Sim (Failover ativo)' : 'Não (Operação normal)'}`
197
+ ].join('\n');
198
+ }
199
+ }
200
+
201
+ class MultiProviderRouter {
202
+ constructor(providers = ['anthropic', 'openai', 'groq', 'ollama'], options = {}) {
203
+ this.circuits = new Map();
204
+ providers.forEach(p => {
205
+ this.circuits.set(p, new CircuitBreaker(p, options));
206
+ });
207
+ this.providerChain = providers;
208
+ }
209
+
210
+ async dispatch(taskFnMap) {
211
+ const attempted = [];
212
+
213
+ for (const provider of this.providerChain) {
214
+ const circuit = this.circuits.get(provider);
215
+ attempted.push(provider);
216
+
217
+ if (!circuit.isOpen()) {
218
+ const fn = taskFnMap[provider] || taskFnMap['default'];
219
+ if (!fn) continue;
220
+
221
+ try {
222
+ const res = await circuit.execute(fn);
223
+ return {
224
+ success: true,
225
+ providerUsed: provider,
226
+ attempted,
227
+ result: res.result,
228
+ circuitStates: this.getAllStatuses()
229
+ };
230
+ } catch (err) {
231
+ // Erro registrado pelo circuit breaker; continua para o próximo provedor na cadeia
232
+ continue;
233
+ }
234
+ }
235
+ }
236
+
237
+ return {
238
+ success: false,
239
+ error: 'Todos os provedores de IA estão indisponíveis ou com circuito ABERTO.',
240
+ attempted,
241
+ circuitStates: this.getAllStatuses()
242
+ };
243
+ }
244
+
245
+ dispatchSync(taskFnMap) {
246
+ const attempted = [];
247
+
248
+ for (const provider of this.providerChain) {
249
+ const circuit = this.circuits.get(provider);
250
+ attempted.push(provider);
251
+
252
+ if (!circuit.isOpen()) {
253
+ const fn = taskFnMap[provider] || taskFnMap['default'];
254
+ if (!fn) continue;
255
+
256
+ try {
257
+ const res = circuit.executeSync(fn);
258
+ return {
259
+ success: true,
260
+ providerUsed: provider,
261
+ attempted,
262
+ result: res.result,
263
+ circuitStates: this.getAllStatuses()
264
+ };
265
+ } catch (err) {
266
+ continue;
267
+ }
268
+ }
269
+ }
270
+
271
+ return {
272
+ success: false,
273
+ error: 'Todos os provedores de IA estão indisponíveis ou com circuito ABERTO.',
274
+ attempted,
275
+ circuitStates: this.getAllStatuses()
276
+ };
277
+ }
278
+
279
+ async executePromptWithFailover(prompt, options = {}) {
280
+ const taskMap = {};
281
+ for (const provider of this.providerChain) {
282
+ taskMap[provider] = () => callLlmHttp(provider, prompt, options);
283
+ }
284
+ return this.dispatch(taskMap);
285
+ }
286
+
287
+ getAllStatuses() {
288
+ const statuses = {};
289
+ for (const [name, circuit] of this.circuits.entries()) {
290
+ statuses[name] = circuit.getStatus();
291
+ }
292
+ return statuses;
293
+ }
294
+
295
+ resetAll() {
296
+ for (const circuit of this.circuits.values()) {
297
+ circuit.reset();
298
+ }
299
+ }
300
+
301
+ auditToSarif() {
302
+ const statuses = this.getAllStatuses();
303
+ const findings = [];
304
+ const openCircuits = [];
305
+
306
+ for (const [name, st] of Object.entries(statuses)) {
307
+ if (st.isOpen) {
308
+ openCircuits.push(name);
309
+ findings.push({
310
+ ruleId: 'RULE-CIRCUIT-OPEN',
311
+ name: 'CircuitBreakerTripped',
312
+ severity: 'HIGH',
313
+ message: `O provedor ${name} está com o circuito ABERTO (${st.failureCount} falhas).`,
314
+ remediation: `Ative o failover automático para outro provedor da cadeia (${this.providerChain.join(', ')}).`
315
+ });
316
+ }
317
+ }
318
+
319
+ return createSarifReport({
320
+ toolName: 'agent-master-llm-router',
321
+ rules: [
322
+ {
323
+ id: 'RULE-CIRCUIT-OPEN',
324
+ name: 'CircuitBreakerTripped',
325
+ description: 'Identifica provedores LLM indisponíveis ou rompidos na cadeia de resiliência',
326
+ severity: 'HIGH',
327
+ cwe: 'CWE-400'
328
+ }
329
+ ],
330
+ findings,
331
+ properties: {
332
+ totalProviders: this.providerChain.length,
333
+ openCircuitsCount: openCircuits.length,
334
+ openCircuits
335
+ }
336
+ });
337
+ }
338
+
339
+ formatReport() {
340
+ const statuses = this.getAllStatuses();
341
+ const rows = Object.entries(statuses).map(([name, s]) =>
342
+ `| \`${name}\` | \`${s.state}\` | ${s.failureCount}/${s.failureThreshold} | ${s.isOpen ? '⚠️ Sim' : '✅ Não'} |`
343
+ ).join('\n');
344
+
345
+ return [
346
+ '### Relatório de Resiliência — Roteador Multi-Provedor LLM',
347
+ '| Provedor | Estado | Falhas | Circuito Aberto |',
348
+ '|---|---|---|---|',
349
+ rows
350
+ ].join('\n');
351
+ }
352
+ }
353
+
354
+ function callLlmHttp(provider, prompt, options = {}) {
355
+ if (options.transportAdapter) {
356
+ return options.transportAdapter(provider, prompt, options);
357
+ }
358
+
359
+ const timeoutMs = options.timeoutMs || 10000;
360
+
361
+ return new Promise((resolve, reject) => {
362
+ let urlStr = '';
363
+ const headers = { 'Content-Type': 'application/json' };
364
+ let payload = {};
365
+
366
+ if (provider === 'openai') {
367
+ const apiKey = options.openaiApiKey || process.env.OPENAI_API_KEY;
368
+ if (!apiKey) return reject(new Error('OPENAI_API_KEY não configurada no ambiente.'));
369
+ urlStr = 'https://api.openai.com/v1/chat/completions';
370
+ headers['Authorization'] = `Bearer ${apiKey}`;
371
+ payload = {
372
+ model: options.openaiModel || 'gpt-4o-mini',
373
+ messages: [{ role: 'user', content: prompt }]
374
+ };
375
+ } else if (provider === 'anthropic') {
376
+ const apiKey = options.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
377
+ if (!apiKey) return reject(new Error('ANTHROPIC_API_KEY não configurada no ambiente.'));
378
+ urlStr = 'https://api.anthropic.com/v1/messages';
379
+ headers['x-api-key'] = apiKey;
380
+ headers['anthropic-version'] = '2023-06-01';
381
+ payload = {
382
+ model: options.anthropicModel || 'claude-3-5-haiku-20241022',
383
+ max_tokens: 1024,
384
+ messages: [{ role: 'user', content: prompt }]
385
+ };
386
+ } else if (provider === 'groq') {
387
+ const apiKey = options.groqApiKey || process.env.GROQ_API_KEY;
388
+ if (!apiKey) return reject(new Error('GROQ_API_KEY não configurada no ambiente.'));
389
+ urlStr = 'https://api.groq.com/openai/v1/chat/completions';
390
+ headers['Authorization'] = `Bearer ${apiKey}`;
391
+ payload = {
392
+ model: options.groqModel || 'llama-3.3-70b-versatile',
393
+ messages: [{ role: 'user', content: prompt }]
394
+ };
395
+ } else if (provider === 'ollama') {
396
+ const host = options.ollamaHost || process.env.OLLAMA_HOST || 'http://localhost:11434';
397
+ urlStr = `${host.replace(/\/+$/, '')}/api/generate`;
398
+ payload = {
399
+ model: options.ollamaModel || 'llama3',
400
+ prompt,
401
+ stream: false
402
+ };
403
+ } else {
404
+ return reject(new Error(`Provedor desconhecido: ${provider}`));
405
+ }
406
+
407
+ const parsed = new URL(urlStr);
408
+ const transport = parsed.protocol === 'https:' ? require('node:https') : require('node:http');
409
+ const bodyStr = JSON.stringify(payload);
410
+ headers['Content-Length'] = Buffer.byteLength(bodyStr);
411
+
412
+ const req = transport.request(parsed, {
413
+ method: 'POST',
414
+ headers,
415
+ timeout: timeoutMs
416
+ }, (res) => {
417
+ let data = '';
418
+ res.setEncoding('utf8');
419
+ res.on('data', chunk => { data += chunk; });
420
+ res.on('end', () => {
421
+ if (res.statusCode >= 200 && res.statusCode < 300) {
422
+ try {
423
+ const parsedRes = JSON.parse(data);
424
+ let textOutput = '';
425
+ if (provider === 'anthropic') {
426
+ textOutput = parsedRes.content && parsedRes.content[0] ? parsedRes.content[0].text : '';
427
+ } else if (provider === 'ollama') {
428
+ textOutput = parsedRes.response || '';
429
+ } else {
430
+ textOutput = parsedRes.choices && parsedRes.choices[0] && parsedRes.choices[0].message ? parsedRes.choices[0].message.content : '';
431
+ }
432
+ resolve({
433
+ provider,
434
+ statusCode: res.statusCode,
435
+ text: textOutput,
436
+ raw: parsedRes
437
+ });
438
+ } catch (jsonErr) {
439
+ resolve({ provider, statusCode: res.statusCode, text: data });
440
+ }
441
+ } else {
442
+ reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Erro de API'} - ${data.slice(0, 200)}`));
443
+ }
444
+ });
445
+ });
446
+
447
+ req.on('timeout', () => {
448
+ req.destroy();
449
+ reject(new Error(`Timeout de conexão após ${timeoutMs}ms ao chamar ${provider}.`));
450
+ });
451
+
452
+ req.on('error', (err) => {
453
+ reject(new Error(`Erro de rede no provedor ${provider}: ${err.message}`));
454
+ });
455
+
456
+ req.write(bodyStr);
457
+ req.end();
458
+ });
459
+ }
460
+
461
+
462
+ async function runCli() {
463
+ console.log('=== Simulação de Resiliência Multi-Provedor com Circuit Breaker (I-10) ===');
464
+ const router = new MultiProviderRouter(['anthropic', 'openai', 'groq', 'ollama'], { failureThreshold: 2 });
465
+
466
+ // Simula indisponibilidade em anthropic e openai
467
+ const taskMap = {
468
+ anthropic: async () => { throw new Error('HTTP 429: Too Many Requests (Rate limit exceeded)'); },
469
+ openai: async () => { throw new Error('HTTP 503: Service Unavailable'); },
470
+ groq: async () => { return 'Resposta gerada com sucesso via Groq (Llama 3.3 70B)'; },
471
+ ollama: async () => { return 'Resposta local via Ollama'; }
472
+ };
473
+
474
+ const dispatch1 = await router.dispatch(taskMap);
475
+ console.log('\nDespacho 1 (Anthropic & OpenAI falham -> Fallback para Groq):');
476
+ console.log(`Provedor Utilizado: ${dispatch1.providerUsed}`);
477
+ console.log(`Tentativas: ${dispatch1.attempted.join(' -> ')}`);
478
+ console.log(`Resultado: ${dispatch1.result}`);
479
+
480
+ console.log('\nStatus dos Circuitos:');
481
+ console.log(JSON.stringify(dispatch1.circuitStates, null, 2));
482
+ }
483
+
484
+ if (require.main === module) {
485
+ runCli().catch(console.error);
486
+ }
487
+
488
+
489
+ class AntiThrashingGuard {
490
+ constructor(options = {}) {
491
+ this.maxIdenticalFailures = options.maxIdenticalFailures || 2;
492
+ this.history = [];
493
+ this.thrashingTripped = false;
494
+ }
495
+
496
+ normalizeError(error) {
497
+ if (!error) return 'unknown_error';
498
+ const msg = typeof error === 'string' ? error : (error.message || String(error));
499
+ return msg
500
+ .replace(/:\d+:\d+/g, '')
501
+ .replace(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\b/g, '')
502
+ .replace(/0x[0-9a-fA-F]+/g, '')
503
+ .trim();
504
+ }
505
+
506
+ recordAttempt(error = null) {
507
+ if (!error) {
508
+ this.history = [];
509
+ this.thrashingTripped = false;
510
+ return { isThrashing: false, count: 0 };
511
+ }
512
+
513
+ const norm = this.normalizeError(error);
514
+ this.history.push({ signature: norm, timestamp: Date.now() });
515
+
516
+ if (this.history.length >= this.maxIdenticalFailures) {
517
+ const recent = this.history.slice(-this.maxIdenticalFailures);
518
+ const allIdentical = recent.every(h => h.signature === norm);
519
+ if (allIdentical) {
520
+ this.thrashingTripped = true;
521
+ return {
522
+ isThrashing: true,
523
+ count: this.history.length,
524
+ signature: norm,
525
+ message: `[ANTI-THRASHING FINOPS VETO]: Detectadas ${this.maxIdenticalFailures} falhas consecutivas com erro idêntico ("${norm.substring(0, 80)}..."). Execução autônoma interrompida para evitar queima descontrolada de tokens.`
526
+ };
527
+ }
528
+ }
529
+
530
+ return { isThrashing: false, count: this.history.length, signature: norm };
531
+ }
532
+
533
+ reset() {
534
+ this.history = [];
535
+ this.thrashingTripped = false;
536
+ }
537
+ }
538
+
539
+
540
+ module.exports = {
541
+ STATES,
542
+ CircuitBreaker,
543
+ MultiProviderRouter,
544
+ AntiThrashingGuard,
545
+ callLlmHttp
546
+ };