atabey 0.0.14 → 0.0.16

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 (159) hide show
  1. package/LICENSE +672 -21
  2. package/PRIVACY.md +8 -14
  3. package/README.md +183 -25
  4. package/bin/cli.js +72 -5
  5. package/dist/framework-mcp/src/index.js +145 -13
  6. package/dist/framework-mcp/src/index.js.map +1 -1
  7. package/dist/framework-mcp/src/tools/control_plane/registry.d.ts +1 -1
  8. package/dist/framework-mcp/src/tools/control_plane/registry.js +52 -4
  9. package/dist/framework-mcp/src/tools/control_plane/registry.js.map +1 -1
  10. package/dist/framework-mcp/src/tools/definitions.js +1 -1
  11. package/dist/framework-mcp/src/tools/file_system/read_file.js +5 -0
  12. package/dist/framework-mcp/src/tools/file_system/read_file.js.map +1 -1
  13. package/dist/framework-mcp/src/tools/messaging/approve_operation.d.ts +10 -8
  14. package/dist/framework-mcp/src/tools/messaging/approve_operation.js +12 -12
  15. package/dist/framework-mcp/src/tools/messaging/approve_operation.js.map +1 -1
  16. package/dist/framework-mcp/src/tools/messaging/ask_human.d.ts +8 -8
  17. package/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
  18. package/dist/framework-mcp/src/tools/messaging/send_message.js +19 -3
  19. package/dist/framework-mcp/src/tools/messaging/send_message.js.map +1 -1
  20. package/dist/framework-mcp/src/tools/observability/check_ports.d.ts +1 -0
  21. package/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
  22. package/dist/framework-mcp/src/tools/observability/check_ports.js.map +1 -1
  23. package/dist/framework-mcp/src/tools/schemas.d.ts +4 -4
  24. package/dist/framework-mcp/src/tools/search/grep_search.js +11 -4
  25. package/dist/framework-mcp/src/tools/search/grep_search.js.map +1 -1
  26. package/dist/framework-mcp/src/tools/search/list_dir.js +3 -0
  27. package/dist/framework-mcp/src/tools/search/list_dir.js.map +1 -1
  28. package/dist/framework-mcp/src/tools/shell/run_command.js +185 -93
  29. package/dist/framework-mcp/src/tools/shell/run_command.js.map +1 -1
  30. package/dist/framework-mcp/src/utils/auth.d.ts +26 -2
  31. package/dist/framework-mcp/src/utils/auth.js +74 -7
  32. package/dist/framework-mcp/src/utils/auth.js.map +1 -1
  33. package/dist/framework-mcp/src/utils/compliance.js +45 -13
  34. package/dist/framework-mcp/src/utils/compliance.js.map +1 -1
  35. package/dist/framework-mcp/src/utils/discipline.d.ts +1 -0
  36. package/dist/framework-mcp/src/utils/discipline.js +42 -3
  37. package/dist/framework-mcp/src/utils/discipline.js.map +1 -1
  38. package/dist/framework-mcp/src/utils/human-in-loop.d.ts +35 -2
  39. package/dist/framework-mcp/src/utils/human-in-loop.js +114 -10
  40. package/dist/framework-mcp/src/utils/human-in-loop.js.map +1 -1
  41. package/dist/framework-mcp/src/utils/loop-detector.js +3 -3
  42. package/dist/framework-mcp/src/utils/loop-detector.js.map +1 -1
  43. package/dist/framework-mcp/src/utils/permissions.d.ts +67 -3
  44. package/dist/framework-mcp/src/utils/permissions.js +179 -43
  45. package/dist/framework-mcp/src/utils/permissions.js.map +1 -1
  46. package/dist/framework-mcp/src/utils/prompt-injection.d.ts +20 -0
  47. package/dist/framework-mcp/src/utils/prompt-injection.js +64 -0
  48. package/dist/framework-mcp/src/utils/prompt-injection.js.map +1 -0
  49. package/dist/framework-mcp/src/utils/quality.js +4 -3
  50. package/dist/framework-mcp/src/utils/quality.js.map +1 -1
  51. package/dist/framework-mcp/src/utils/silent-router.d.ts +4 -2
  52. package/dist/framework-mcp/src/utils/silent-router.js +35 -9
  53. package/dist/framework-mcp/src/utils/silent-router.js.map +1 -1
  54. package/dist/framework-mcp/tests/tools/file_system/permissions.test.js +174 -71
  55. package/dist/framework-mcp/tests/tools/file_system/permissions.test.js.map +1 -1
  56. package/dist/framework-mcp/tests/tools/messaging/send_message.test.js +36 -30
  57. package/dist/framework-mcp/tests/tools/messaging/send_message.test.js.map +1 -1
  58. package/dist/framework-mcp/tests/tools/observability/check_ports.test.js +26 -12
  59. package/dist/framework-mcp/tests/tools/observability/check_ports.test.js.map +1 -1
  60. package/dist/framework-mcp/tests/tools/shell/run_command.test.js +38 -15
  61. package/dist/framework-mcp/tests/tools/shell/run_command.test.js.map +1 -1
  62. package/dist/framework-mcp/tests/utils/human-in-loop.test.js +152 -4
  63. package/dist/framework-mcp/tests/utils/human-in-loop.test.js.map +1 -1
  64. package/dist/framework-mcp/tests/utils/loop-detector.test.js +0 -2
  65. package/dist/framework-mcp/tests/utils/loop-detector.test.js.map +1 -1
  66. package/dist/framework-mcp/tests/utils/prompt-injection.test.d.ts +1 -0
  67. package/dist/framework-mcp/tests/utils/prompt-injection.test.js +43 -0
  68. package/dist/framework-mcp/tests/utils/prompt-injection.test.js.map +1 -0
  69. package/dist/src/cli/commands/dashboard.js +167 -2
  70. package/dist/src/cli/commands/dashboard.js.map +1 -1
  71. package/dist/src/cli/commands/init/scaffold-standards.js +1 -1
  72. package/dist/src/cli/commands/init/scaffold-standards.js.map +1 -1
  73. package/dist/src/cli/commands/init.js +14 -7
  74. package/dist/src/cli/commands/init.js.map +1 -1
  75. package/dist/src/cli/commands/log.js +8 -5
  76. package/dist/src/cli/commands/log.js.map +1 -1
  77. package/dist/src/cli/commands/mcp.js +3 -1
  78. package/dist/src/cli/commands/mcp.js.map +1 -1
  79. package/dist/src/cli/commands/orchestrate.d.ts +4 -4
  80. package/dist/src/cli/commands/orchestrate.js +14 -2
  81. package/dist/src/cli/commands/orchestrate.js.map +1 -1
  82. package/dist/src/cli/commands/script.js +8 -1
  83. package/dist/src/cli/commands/script.js.map +1 -1
  84. package/dist/src/cli/commands/status.js +21 -1
  85. package/dist/src/cli/commands/status.js.map +1 -1
  86. package/dist/src/cli/utils/memory.js +6 -5
  87. package/dist/src/cli/utils/memory.js.map +1 -1
  88. package/dist/src/cli/utils/schemas.d.ts +2 -2
  89. package/dist/src/contracts/tasks.d.ts +4 -4
  90. package/dist/src/modules/contracts/api.d.ts +6 -6
  91. package/dist/src/modules/engines/agent-executor.js +41 -20
  92. package/dist/src/modules/engines/agent-executor.js.map +1 -1
  93. package/dist/src/modules/engines/agent-loop.d.ts +5 -5
  94. package/dist/src/modules/engines/agent-loop.js +5 -5
  95. package/dist/src/modules/engines/evaluation-engine.d.ts +9 -2
  96. package/dist/src/modules/engines/evaluation-engine.js +77 -9
  97. package/dist/src/modules/engines/evaluation-engine.js.map +1 -1
  98. package/dist/src/modules/engines/quality-gate.js +4 -2
  99. package/dist/src/modules/engines/quality-gate.js.map +1 -1
  100. package/dist/src/modules/engines/risk-engine.d.ts +17 -1
  101. package/dist/src/modules/engines/risk-engine.js +98 -3
  102. package/dist/src/modules/engines/risk-engine.js.map +1 -1
  103. package/dist/src/modules/engines/routing-engine.d.ts +26 -1
  104. package/dist/src/modules/engines/routing-engine.js +97 -1
  105. package/dist/src/modules/engines/routing-engine.js.map +1 -1
  106. package/dist/src/modules/providers/shared.js +2 -3
  107. package/dist/src/modules/providers/shared.js.map +1 -1
  108. package/dist/src/shared/pii.js +1 -1
  109. package/dist/src/shared/pii.js.map +1 -1
  110. package/dist/src/shared/storage.d.ts +60 -0
  111. package/dist/src/shared/storage.js +77 -3
  112. package/dist/src/shared/storage.js.map +1 -1
  113. package/dist/tests/cli/commands/dashboard-coverage.test.js +3 -1
  114. package/dist/tests/cli/commands/dashboard-coverage.test.js.map +1 -1
  115. package/dist/tests/modules/engines/agent-executor.test.js +1 -1
  116. package/dist/tests/modules/engines/agent-executor.test.js.map +1 -1
  117. package/dist/tests/modules/engines/evaluation-engine.test.js +2 -2
  118. package/dist/tests/modules/engines/evaluation-engine.test.js.map +1 -1
  119. package/dist/tests/status-cost.test.js +2 -0
  120. package/dist/tests/status-cost.test.js.map +1 -1
  121. package/dist/tests/storage-operations.test.js +31 -4
  122. package/dist/tests/storage-operations.test.js.map +1 -1
  123. package/dist/vitest.config.js +6 -4
  124. package/dist/vitest.config.js.map +1 -1
  125. package/framework-mcp/README.md +18 -32
  126. package/framework-mcp/dist/dashboard/assets/index-B-Eu7_J9.js +1260 -0
  127. package/framework-mcp/dist/dashboard/index.html +1 -1
  128. package/framework-mcp/dist/framework-mcp/src/index.js +145 -13
  129. package/framework-mcp/dist/framework-mcp/src/tools/control_plane/registry.js +52 -4
  130. package/framework-mcp/dist/framework-mcp/src/tools/definitions.js +1 -1
  131. package/framework-mcp/dist/framework-mcp/src/tools/file_system/read_file.js +5 -0
  132. package/framework-mcp/dist/framework-mcp/src/tools/messaging/approve_operation.js +12 -12
  133. package/framework-mcp/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
  134. package/framework-mcp/dist/framework-mcp/src/tools/messaging/send_message.js +19 -3
  135. package/framework-mcp/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
  136. package/framework-mcp/dist/framework-mcp/src/tools/search/grep_search.js +11 -4
  137. package/framework-mcp/dist/framework-mcp/src/tools/search/list_dir.js +3 -0
  138. package/framework-mcp/dist/framework-mcp/src/tools/shell/run_command.js +185 -93
  139. package/framework-mcp/dist/framework-mcp/src/utils/auth.js +74 -7
  140. package/framework-mcp/dist/framework-mcp/src/utils/compliance.js +45 -13
  141. package/framework-mcp/dist/framework-mcp/src/utils/discipline.js +42 -3
  142. package/framework-mcp/dist/framework-mcp/src/utils/human-in-loop.js +114 -10
  143. package/framework-mcp/dist/framework-mcp/src/utils/loop-detector.js +3 -3
  144. package/framework-mcp/dist/framework-mcp/src/utils/permissions.js +179 -43
  145. package/framework-mcp/dist/framework-mcp/src/utils/prompt-injection.js +63 -0
  146. package/framework-mcp/dist/framework-mcp/src/utils/quality.js +4 -3
  147. package/framework-mcp/dist/framework-mcp/src/utils/silent-router.js +35 -9
  148. package/framework-mcp/dist/src/cli/utils/memory.js +6 -5
  149. package/framework-mcp/dist/src/modules/engines/evaluation-engine.js +170 -0
  150. package/framework-mcp/dist/src/modules/engines/quality-gate.js +4 -2
  151. package/framework-mcp/dist/src/modules/engines/risk-engine.js +98 -3
  152. package/framework-mcp/dist/src/modules/engines/routing-engine.js +97 -1
  153. package/framework-mcp/dist/src/modules/providers/shared.js +2 -3
  154. package/framework-mcp/dist/src/shared/pii.js +1 -1
  155. package/framework-mcp/dist/src/shared/storage.js +77 -3
  156. package/framework-mcp/package.json +21 -18
  157. package/mcp.json +3 -2
  158. package/package.json +4 -5
  159. package/framework-mcp/dist/dashboard/assets/index-B2mYld0c.js +0 -1221
@@ -31,7 +31,49 @@ const CONFIG = {
31
31
  AUTO_APPROVE_LOW_RISK: process.env.MCP_AUTO_APPROVE_LOW_RISK !== "false",
32
32
  /** Approval timeout in seconds */
33
33
  APPROVAL_TIMEOUT_SECONDS: parseInt(process.env.MCP_APPROVAL_TIMEOUT || "300", 10), // 5 min
34
+ /**
35
+ * Minimum risk score requiring Lead/Admin approval (legacy compat).
36
+ * Superseded by ROLE_RISK_MAP below — kept for backward compatibility with env var.
37
+ */
38
+ RBAC_HIGH_RISK_THRESHOLD: parseInt(process.env.MCP_RBAC_HIGH_RISK_THRESHOLD || "80", 10),
34
39
  };
40
+ /**
41
+ * Maximum risk score each role is allowed to approve.
42
+ * A role can approve any operation with riskScore <= its max.
43
+ * Override via MCP_RBAC_ROLE_* env vars.
44
+ *
45
+ * junior → cannot approve anything (max = -1)
46
+ * developer → can approve MEDIUM risk (0–59)
47
+ * lead → can approve HIGH risk (0–79)
48
+ * admin → can approve any risk (0–100)
49
+ */
50
+ export const ROLE_RISK_MAP = {
51
+ admin: parseInt(process.env.MCP_RBAC_ROLE_ADMIN || "100", 10),
52
+ lead: parseInt(process.env.MCP_RBAC_ROLE_LEAD || "79", 10),
53
+ developer: parseInt(process.env.MCP_RBAC_ROLE_DEVELOPER || "59", 10),
54
+ junior: parseInt(process.env.MCP_RBAC_ROLE_JUNIOR || "-1", 10), // cannot approve
55
+ };
56
+ /**
57
+ * Returns the minimum role required to approve an operation at the given risk score.
58
+ */
59
+ export function getMinimumRoleForScore(riskScore) {
60
+ if (riskScore <= ROLE_RISK_MAP.developer)
61
+ return "developer";
62
+ if (riskScore <= ROLE_RISK_MAP.lead)
63
+ return "lead";
64
+ return "admin";
65
+ }
66
+ /**
67
+ * Reads the active user role from environment only.
68
+ * AI-supplied userRole arguments are NOT trusted.
69
+ */
70
+ function getActiveUserRole() {
71
+ const envRole = process.env.MCP_USER_ROLE?.trim().toLowerCase();
72
+ if (envRole === "admin" || envRole === "lead" || envRole === "developer" || envRole === "junior") {
73
+ return envRole;
74
+ }
75
+ return "junior"; // safest default
76
+ }
35
77
  // ─── Active Approval Requests ─────────────────────────────────────
36
78
  const activeApprovals = new Map();
37
79
  /**
@@ -44,12 +86,25 @@ export function getRiskLevel(score) {
44
86
  return "MEDIUM";
45
87
  return "LOW";
46
88
  }
89
+ /**
90
+ * Create an approval request for a high-risk operation.
91
+ * Returns the approval request and a message for the AI/developer.
92
+ */
93
+ function generatePasscode() {
94
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
95
+ let code = "";
96
+ for (let i = 0; i < 5; i++) {
97
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
98
+ }
99
+ return code;
100
+ }
47
101
  /**
48
102
  * Create an approval request for a high-risk operation.
49
103
  * Returns the approval request and a message for the AI/developer.
50
104
  */
51
105
  export function createApprovalRequest(traceId, toolName, agent, riskScore, reason, args) {
52
106
  const now = Date.now();
107
+ const passcode = generatePasscode();
53
108
  const request = {
54
109
  traceId,
55
110
  toolName,
@@ -61,8 +116,11 @@ export function createApprovalRequest(traceId, toolName, agent, riskScore, reaso
61
116
  createdAt: now,
62
117
  expiresAt: now + CONFIG.APPROVAL_TIMEOUT_SECONDS * 1000,
63
118
  status: "PENDING",
119
+ passcode,
64
120
  };
65
121
  activeApprovals.set(traceId, request);
122
+ // Print passcode to stderr so the human developer sees it but AI cannot access it
123
+ process.stderr.write(`\n[ATABEY SECURITY GATES] APPROVAL PASSCODE FOR TRACE ${traceId}: ${passcode}\n\n`);
66
124
  // Create a Hermes approval message
67
125
  AtabeyStorage.saveMessage({
68
126
  from: asAgentID(agent),
@@ -92,14 +150,14 @@ export function createApprovalRequest(traceId, toolName, agent, riskScore, reaso
92
150
  ` Trace ID: ${traceId}`,
93
151
  "",
94
152
  "─── IN-CHAT APPROVAL ────────────────────────────────────────",
95
- "Ask your AI assistant to call the approve_operation tool:",
153
+ "This is a high-risk operation. To prevent autonomous bypass,",
154
+ "Atabey has printed a 5-character PASSCODE to your terminal's stderr.",
96
155
  "",
97
- ` To approve: approve_operation { "action": "approve", "traceId": "${traceId}" }`,
98
- ` To reject: approve_operation { "action": "reject", "traceId": "${traceId}" }`,
99
- " To list: approve_operation { \"action\": \"list\" }",
156
+ "Ask the user for the passcode, then call the approve_operation tool:",
157
+ ` approve_operation { "action": "approve", "traceId": "${traceId}", "passcode": "<passcode_from_user>" }`,
100
158
  "─────────────────────────────────────────────────────────────",
101
159
  "",
102
- `Alternative: atabey hitl answer "${traceId}:approve" in your terminal.`,
160
+ `Alternative: Run 'atabey approve ${traceId}' in your local terminal.`,
103
161
  ].join("\n");
104
162
  return { request, message };
105
163
  }
@@ -147,8 +205,16 @@ export function getApprovalStatus(traceId) {
147
205
  }
148
206
  /**
149
207
  * Approve a pending operation.
208
+ *
209
+ * RBAC enforcement (AI Governance layer):
210
+ * - Role is read from process.env.MCP_USER_ROLE ONLY.
211
+ * - The userRole parameter is accepted for API compatibility but completely IGNORED.
212
+ * This prevents AI agents (Claude Code, Gemini, Cursor) from escalating privilege
213
+ * by passing {userRole: "admin"} in the tool call arguments.
214
+ * - The passcode printed to stderr is the second factor — AI cannot read stderr.
150
215
  */
151
- export function approveOperation(traceId) {
216
+ export function approveOperation(traceId, passcode, _userRoleIgnored // kept for API compat — value is intentionally ignored
217
+ ) {
152
218
  const request = activeApprovals.get(traceId);
153
219
  if (!request) {
154
220
  return { success: false, message: `No pending approval found for trace: ${traceId}` };
@@ -156,6 +222,29 @@ export function approveOperation(traceId) {
156
222
  if (request.status !== "PENDING") {
157
223
  return { success: false, message: `Approval for ${traceId} is already ${request.status}` };
158
224
  }
225
+ // ── RBAC Check ──────────────────────────────────────────────────────────
226
+ // Role is read exclusively from the environment — not from the tool argument.
227
+ // This ensures Claude Code, Gemini CLI, Cursor, or any MCP client cannot
228
+ // escalate privilege by injecting a role in the tool args.
229
+ const role = getActiveUserRole();
230
+ const maxApprovable = ROLE_RISK_MAP[role];
231
+ if (request.riskScore <= maxApprovable) {
232
+ // Role is allowed — proceed to passcode check
233
+ }
234
+ else {
235
+ const requiredRole = getMinimumRoleForScore(request.riskScore);
236
+ return {
237
+ success: false,
238
+ message: `[RBAC] Approval denied: Risk score ${request.riskScore}/100 requires at least '${requiredRole}' role. ` +
239
+ `Current role: '${role}' (max approvable: ${maxApprovable}). ` +
240
+ "Set MCP_USER_ROLE env var to a role with sufficient authority."
241
+ };
242
+ }
243
+ // ── End RBAC Check ──────────────────────────────────────────────────────
244
+ // Passcode second-factor (printed to stderr — AI cannot intercept)
245
+ if (request.passcode && (!passcode || passcode.trim().toUpperCase() !== request.passcode)) {
246
+ return { success: false, message: "Invalid or missing approval passcode. Verify the passcode printed to stderr." };
247
+ }
159
248
  request.status = "APPROVED";
160
249
  // Update Hermes message status
161
250
  const messages = AtabeyStorage.getPendingMessages();
@@ -165,12 +254,17 @@ export function approveOperation(traceId) {
165
254
  AtabeyStorage.updateMessageStatus(msg.id, "APPROVED");
166
255
  }
167
256
  }
168
- return { success: true, message: `Operation ${traceId} approved. You may retry.` };
257
+ return { success: true, message: `Operation ${traceId} approved by '${role}'. You may retry.` };
169
258
  }
170
259
  /**
171
260
  * Reject a pending operation.
261
+ *
262
+ * RBAC: junior-role users cannot reject HIGH/CRITICAL operations.
263
+ * This prevents rogue or misconfigured recon agents from killing
264
+ * manager-delegated critical operations.
172
265
  */
173
- export function rejectOperation(traceId) {
266
+ export function rejectOperation(traceId, _userRoleIgnored // kept for API compat — value intentionally ignored
267
+ ) {
174
268
  const request = activeApprovals.get(traceId);
175
269
  if (!request) {
176
270
  return { success: false, message: `No pending approval found for trace: ${traceId}` };
@@ -178,6 +272,16 @@ export function rejectOperation(traceId) {
178
272
  if (request.status !== "PENDING") {
179
273
  return { success: false, message: `Approval for ${traceId} is already ${request.status}` };
180
274
  }
275
+ // RBAC: reject also requires minimum role for HIGH+ operations
276
+ const role = getActiveUserRole();
277
+ const canReject = !(role === "junior" && request.riskScore >= CONFIG.HIGH_RISK_THRESHOLD);
278
+ if (!canReject) {
279
+ return {
280
+ success: false,
281
+ message: `[RBAC] Reject denied: Risk score ${request.riskScore}/100 operations cannot be rejected by '${role}'. ` +
282
+ "Minimum role required to reject is 'developer'. Set MCP_USER_ROLE env var."
283
+ };
284
+ }
181
285
  request.status = "REJECTED";
182
286
  // Update Hermes message status
183
287
  const messages = AtabeyStorage.getPendingMessages();
@@ -187,7 +291,7 @@ export function rejectOperation(traceId) {
187
291
  AtabeyStorage.updateMessageStatus(msg.id, "REJECTED");
188
292
  }
189
293
  }
190
- return { success: true, message: `Operation ${traceId} rejected.` };
294
+ return { success: true, message: `Operation ${traceId} rejected by '${role}'.` };
191
295
  }
192
296
  /**
193
297
  * Get all pending approval requests.
@@ -196,7 +300,7 @@ export function getPendingApprovals() {
196
300
  const now = Date.now();
197
301
  const pending = [];
198
302
  // Clean expired
199
- for (const [traceId, request] of activeApprovals) {
303
+ for (const [, request] of activeApprovals) {
200
304
  if (request.status === "PENDING" && now > request.expiresAt) {
201
305
  request.status = "EXPIRED";
202
306
  }
@@ -274,7 +274,7 @@ export function recordAndCheck(agent, toolName, args) {
274
274
  /**
275
275
  * Apply cooldown to an agent.
276
276
  */
277
- function applyCooldown(history, alert, agent) {
277
+ function applyCooldown(history, alert, _agent) {
278
278
  history.inCooldown = true;
279
279
  history.cooldownUntil = alert.cooldownUntil || Date.now() + CONFIG.COOLDOWN_MS;
280
280
  history.cooldownCount++;
@@ -347,8 +347,8 @@ export function getLoopStats(agent) {
347
347
  */
348
348
  export function getAllLoopStats() {
349
349
  const stats = {};
350
- for (const [agent] of agentHistories) {
351
- stats[agent] = getLoopStats(agent);
350
+ for (const [agentName] of agentHistories) {
351
+ stats[agentName] = getLoopStats(agentName);
352
352
  }
353
353
  return stats;
354
354
  }
@@ -1,6 +1,50 @@
1
+ /**
2
+ * ─── RBAC PERMISSION MATRIX ───────────────────────────────────────────────
3
+ *
4
+ * Enforces agent-scoped file system permissions at the MCP tool layer.
5
+ * This is the file-level RBAC enforcement point for:
6
+ * - Claude Code (calls write_file / read_file via MCP sub-agents)
7
+ * - Gemini CLI (calls write_file / read_file via MCP server)
8
+ * - Cursor (calls write_file / read_file via MCP rules)
9
+ * - Codex CLI (calls write_file / read_file via agent allowed-tools)
10
+ * - Antigravity (calls write_file / read_file via customAgentSpec tools)
11
+ *
12
+ * Permission matrix is stored in `.atabey/permission-matrix.json`:
13
+ * {
14
+ * "@frontend": { "write": ["apps/web/**"], "read": ["apps/web/**", "docs/**"] },
15
+ * "@backend": { "write": ["apps/backend/**"], "read": ["apps/backend/**", "apps/web/types/**"] }
16
+ * }
17
+ *
18
+ * Agent Tier enforcement:
19
+ * supreme (@manager, @security) — no write/read restrictions (omnipotent)
20
+ * core (@backend, @frontend, ...) — matrix-enforced
21
+ * recon (@explorer, @git, @analyst, @native) — read-only by default,
22
+ * write requires explicit matrix entry
23
+ */
1
24
  import fs from "fs";
2
25
  import path from "path";
3
26
  import { resolveFrameworkDir } from "./security.js";
27
+ // ─── Tier hierarchy ─────────────────────────────────────────────────────────
28
+ /** Well-known tier for agents defined in the built-in registry */
29
+ const AGENT_TIER_MAP = {
30
+ "@manager": "supreme",
31
+ "@security": "supreme",
32
+ "@backend": "core",
33
+ "@frontend": "core",
34
+ "@mobile": "core",
35
+ "@quality": "core",
36
+ "@database": "core",
37
+ "@devops": "core",
38
+ "@architect": "core",
39
+ "@explorer": "recon",
40
+ "@git": "recon",
41
+ "@analyst": "recon",
42
+ "@native": "recon",
43
+ };
44
+ export function getAgentTier(agentName) {
45
+ return AGENT_TIER_MAP[agentName] ?? "core"; // unknown agents default to core
46
+ }
47
+ // ─── Glob → RegExp ──────────────────────────────────────────────────────────
4
48
  function globToRegex(glob) {
5
49
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
6
50
  const step1 = escaped.replace(/\*\*/g, "__DBL_STR__");
@@ -8,64 +52,156 @@ function globToRegex(glob) {
8
52
  const regexStr = "^" + step2.replace(/__DBL_STR__/g, ".*") + "$";
9
53
  return new RegExp(regexStr);
10
54
  }
55
+ // ─── Active Agent Resolution ─────────────────────────────────────────────────
11
56
  /**
12
- * Validates if the active agent has write permission for the target file.
13
- * Automatically identifies the active agent by checking the status.json store
14
- * for the agent in the "EXECUTING" state.
57
+ * Resolves the active agent for the current operation.
58
+ *
59
+ * Priority order:
60
+ * 1. ATABEY_ACTIVE_AGENT env var (set by AgentLoop per-invocation — most reliable)
61
+ * 2. `.atabey/memory/status.json` first EXECUTING agent (legacy fallback)
62
+ *
63
+ * Note: The traceId-scoped approach is preferred when AgentLoop sets
64
+ * ATABEY_ACTIVE_AGENT before spawning the MCP tool call.
15
65
  */
16
- export function verifyWritePermission(projectRoot, targetFilePath) {
17
- const frameworkDir = resolveFrameworkDir(projectRoot);
18
- const absoluteFrameworkPath = path.isAbsolute(frameworkDir)
19
- ? frameworkDir
20
- : path.resolve(projectRoot, frameworkDir);
21
- const matrixPath = path.join(absoluteFrameworkPath, "permission-matrix.json");
22
- // If no permission matrix exists, skip enforcement (default allow)
23
- if (!fs.existsSync(matrixPath)) {
24
- return;
66
+ export function resolveActiveAgent(frameworkDir) {
67
+ // 1. Env var (highest reliability — set by orchestration layer)
68
+ const envAgent = process.env.ATABEY_ACTIVE_AGENT?.trim();
69
+ if (envAgent) {
70
+ return envAgent.startsWith("@") ? envAgent : `@${envAgent}`;
25
71
  }
26
- let matrix;
72
+ // 2. status.json fallback
73
+ const statusPath = path.join(frameworkDir, "memory", "status.json");
74
+ if (!fs.existsSync(statusPath))
75
+ return null;
27
76
  try {
28
- matrix = JSON.parse(fs.readFileSync(matrixPath, "utf8"));
77
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
78
+ for (const [agentName, info] of Object.entries(status)) {
79
+ if (info.state === "EXECUTING") {
80
+ return agentName.startsWith("@") ? agentName : `@${agentName}`;
81
+ }
82
+ }
83
+ }
84
+ catch (e) {
85
+ process.stderr.write(`[Permissions] Warning: Failed to read status.json: ${String(e)}\n`);
86
+ }
87
+ return null;
88
+ }
89
+ // ─── Matrix Loader ───────────────────────────────────────────────────────────
90
+ function loadMatrix(frameworkDir) {
91
+ const matrixPath = path.join(frameworkDir, "permission-matrix.json");
92
+ if (!fs.existsSync(matrixPath))
93
+ return null;
94
+ try {
95
+ return JSON.parse(fs.readFileSync(matrixPath, "utf8"));
29
96
  }
30
97
  catch (e) {
31
98
  throw new Error(`Failed to parse permission-matrix.json: ${String(e)}`, { cause: e });
32
99
  }
33
- // Determine the active agent from status.json
34
- const statusPath = path.join(absoluteFrameworkPath, "memory", "status.json");
35
- let activeAgent = null;
36
- if (fs.existsSync(statusPath)) {
37
- try {
38
- const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
39
- // Find an agent that is currently in the EXECUTING state
40
- for (const [agentName, info] of Object.entries(status)) {
41
- const data = info;
42
- if (data.state === "EXECUTING") {
43
- activeAgent = agentName.startsWith("@") ? agentName : `@${agentName}`;
44
- break;
45
- }
46
- }
100
+ }
101
+ /**
102
+ * Internal enforcement logic for both read and write operations.
103
+ *
104
+ * Enforcement logic per tier:
105
+ * - supreme: always allowed (no matrix needed)
106
+ * - core: enforced by matrix; if no rule defined → default-allow
107
+ * - recon: write always denied unless explicit matrix allow
108
+ * read → allowed by default (recon agents must be able to explore)
109
+ */
110
+ function enforcePermission(projectRoot, targetFilePath, accessType) {
111
+ const frameworkDir = resolveFrameworkDir(projectRoot);
112
+ const absoluteFrameworkPath = path.isAbsolute(frameworkDir)
113
+ ? frameworkDir
114
+ : path.resolve(projectRoot, frameworkDir);
115
+ const activeAgent = resolveActiveAgent(absoluteFrameworkPath);
116
+ // No active agent identified → default-allow (non-agent call, e.g. CLI tools)
117
+ if (!activeAgent)
118
+ return;
119
+ const tier = getAgentTier(activeAgent);
120
+ // Supreme agents are omnipotent — no matrix enforcement
121
+ if (tier === "supreme")
122
+ return;
123
+ // Recon agents: block writes unless explicitly allowed in matrix
124
+ if (tier === "recon" && accessType === "write") {
125
+ const matrix = loadMatrix(absoluteFrameworkPath);
126
+ if (!matrix) {
127
+ throw new Error(`Permission Denied: Agent ${activeAgent} (tier: recon) cannot write files. ` +
128
+ "Recon agents (@explorer, @git, @analyst, @native) are read-only by default. " +
129
+ "Add an explicit write rule to permission-matrix.json to grant write access.");
47
130
  }
48
- catch (e) {
49
- // Log warning but don't crash, default to allowing if status can't be parsed
50
- process.stderr.write(`[Permissions] Warning: Failed to read status.json: ${String(e)}\n`);
131
+ const agentRules = matrix[activeAgent];
132
+ if (!agentRules?.write?.length) {
133
+ throw new Error(`Permission Denied: Agent ${activeAgent} (tier: recon) has no write rules in permission-matrix.json. ` +
134
+ "Recon agents are read-only by default.");
51
135
  }
136
+ // Fall through to glob check below
52
137
  }
53
- // If no active executing agent is found, default to allowing
54
- if (!activeAgent) {
138
+ const matrix = loadMatrix(absoluteFrameworkPath);
139
+ // No matrix file → default-allow (matrix is opt-in)
140
+ if (!matrix)
55
141
  return;
56
- }
57
142
  const agentRules = matrix[activeAgent];
58
- // If no rules defined for the agent, default to allowing
59
- if (!agentRules || !agentRules.write) {
143
+ // No rules for this agent default-allow
144
+ if (!agentRules)
145
+ return;
146
+ const rules = accessType === "write" ? agentRules.write : (agentRules.read ?? null);
147
+ // No rule for this access type → default-allow
148
+ if (!rules)
60
149
  return;
61
- }
62
- // Resolve target path relative to project root for glob matching
63
150
  const relativeTargetPath = path.relative(projectRoot, path.resolve(projectRoot, targetFilePath));
64
- const allowed = agentRules.write.some(glob => {
65
- const regex = globToRegex(glob);
66
- return regex.test(relativeTargetPath);
67
- });
151
+ const allowed = rules.some(glob => globToRegex(glob).test(relativeTargetPath));
68
152
  if (!allowed) {
69
- throw new Error(`Permission Denied: Agent ${activeAgent} is not authorized to write to "${relativeTargetPath}". Matrix rules restrict writes to: [${agentRules.write.join(", ")}].`);
153
+ throw new Error(`Permission Denied: Agent ${activeAgent} (tier: ${tier}) is not authorized to ` +
154
+ `${accessType} "${relativeTargetPath}". ` +
155
+ `Matrix rules restrict ${accessType} to: [${rules.join(", ")}].`);
156
+ }
157
+ }
158
+ // ─── Public API ─────────────────────────────────────────────────────────────
159
+ /**
160
+ * Verifies that the active agent has WRITE permission for the target file.
161
+ * Called by: write_file, replace_text, batch_surgical_edit, patch_file
162
+ *
163
+ * Throws a descriptive error if permission is denied.
164
+ */
165
+ export function verifyWritePermission(projectRoot, targetFilePath) {
166
+ enforcePermission(projectRoot, targetFilePath, "write");
167
+ }
168
+ /**
169
+ * Verifies that the active agent has READ permission for the target file.
170
+ * Called by: read_file, view_file (alias)
171
+ *
172
+ * Throws a descriptive error if permission is denied.
173
+ * Note: read enforcement is SOFTER than write — recon agents can read freely
174
+ * unless an explicit read matrix restriction is defined.
175
+ */
176
+ export function verifyReadPermission(projectRoot, targetFilePath) {
177
+ enforcePermission(projectRoot, targetFilePath, "read");
178
+ }
179
+ /**
180
+ * Verifies that the sender agent is allowed to send messages to the target agent
181
+ * based on the AgentTier hierarchy.
182
+ *
183
+ * Messaging rules (AI IDE governance layer):
184
+ * - Any tier → supreme: allowed (escalation always permitted)
185
+ * - core → core: allowed
186
+ * - recon → core: allowed (recon can delegate up)
187
+ * - recon → supreme: allowed
188
+ * - core → recon: BLOCKED (core agents should not delegate down to recon)
189
+ * - supreme → any: allowed
190
+ *
191
+ * Rationale: Recon agents (@explorer, @git) are information-gatherers.
192
+ * If a core agent delegates to a recon agent, it bypasses the quality gate
193
+ * and risk engine — a potential governance gap.
194
+ */
195
+ export function verifyMessagingPermission(fromAgent, toAgent) {
196
+ const fromTier = getAgentTier(fromAgent);
197
+ const toTier = getAgentTier(toAgent);
198
+ // supreme senders: unrestricted
199
+ if (fromTier === "supreme")
200
+ return;
201
+ // core → recon: block (prevents quality-gate bypass via recon delegation)
202
+ if (fromTier === "core" && toTier === "recon") {
203
+ throw new Error(`[RBAC] Messaging violation: Agent ${fromAgent} (tier: core) cannot delegate to ` +
204
+ `${toAgent} (tier: recon). Core agents must delegate up to @manager or peer core agents. ` +
205
+ "Recon agents are read-only information gatherers and cannot execute delegated tasks.");
70
206
  }
71
207
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ─── PROMPT INJECTION PROTECTION ENGINE ────────────────────────────
3
+ *
4
+ * Scans, detects, and neutralizes prompt injection payloads in tool outputs
5
+ * before they are returned to the AI assistant's context.
6
+ *
7
+ * This prevents malicious files, code comments, or external resources from
8
+ * hijacking the AI client (e.g., instructing it to delete files or ignore safety).
9
+ */
10
+ import { logger } from "../../../src/shared/logger.js";
11
+ // Common prompt injection signatures
12
+ const INJECTION_PATTERNS = [
13
+ /ignore\s+(?:all\s+)?instructions/gi,
14
+ /system\s+override/gi,
15
+ /you\s+must\s+now/gi,
16
+ /you\s+are\s+now/gi,
17
+ /bypass\s+governance/gi,
18
+ /bypass\s+safety/gi,
19
+ /bypass\s+atabey/gi,
20
+ /dan\s+mode/gi,
21
+ /developer\s+mode/gi,
22
+ /override\s+system\s+instructions/gi,
23
+ /ignore\s+safety\s+rules/gi,
24
+ /\[system\s+prompt\]/gi,
25
+ /<system_prompt>/gi,
26
+ /disable\s+restrictions/gi
27
+ ];
28
+ export class PromptInjectionProtection {
29
+ /**
30
+ * Scan tool response text for injection patterns.
31
+ * Replaces any matches with a redirection block and logs it.
32
+ */
33
+ static sanitizeResponse(text) {
34
+ if (!text) {
35
+ return { sanitized: text, detected: false, patterns: [] };
36
+ }
37
+ let detected = false;
38
+ const patternsMatched = [];
39
+ let sanitized = text;
40
+ for (const pattern of INJECTION_PATTERNS) {
41
+ if (pattern.test(sanitized)) {
42
+ detected = true;
43
+ const matches = text.match(pattern);
44
+ if (matches) {
45
+ matches.forEach(m => {
46
+ if (!patternsMatched.includes(m)) {
47
+ patternsMatched.push(m);
48
+ }
49
+ });
50
+ }
51
+ sanitized = sanitized.replace(pattern, "[REDACTED PROMPT INJECTION]");
52
+ }
53
+ }
54
+ if (detected) {
55
+ logger.warn(`[INJECTION PROTECTION] Blocked prompt injection pattern in tool response: ${patternsMatched.join(", ")}`);
56
+ }
57
+ return {
58
+ sanitized,
59
+ detected,
60
+ patterns: patternsMatched
61
+ };
62
+ }
63
+ }
@@ -93,9 +93,10 @@ export function analyzePathQuality(projectRoot, targetPath) {
93
93
  }
94
94
  // Check nesting depth
95
95
  lines.forEach((line, idx) => {
96
- const indentMatch = line.match(/^(?:\s{2}){5,}/);
97
- if (indentMatch && !line.trim().startsWith("//") && !line.trim().startsWith("*")) {
98
- const depth = Math.floor(indentMatch[0].length / 2);
96
+ const leadingSpaces = line.match(/^(\s*)/)?.[0].length || 0;
97
+ if (leadingSpaces >= 10 && !line.trim().startsWith("//") && !line.trim().startsWith("*")) {
98
+ const indentSize = (leadingSpaces % 4 === 0) ? 4 : 2;
99
+ const depth = Math.floor(leadingSpaces / indentSize);
99
100
  if (depth > 4) {
100
101
  deepNesting++;
101
102
  issues.push({
@@ -9,10 +9,11 @@
9
9
  *
10
10
  * Features:
11
11
  * - Auto-agent detection from tool call context
12
- * - Silent system prompt injection
12
+ * - Silent system prompt injection with specialty memory
13
13
  * - Multi-agent chain support
14
14
  * - Stealth notifications (stderr/dashboard only, never chat)
15
15
  */
16
+ import { EvaluationEngine } from "../../../src/modules/engines/evaluation-engine.js";
16
17
  import { RoutingEngine } from "../../../src/modules/engines/routing-engine.js";
17
18
  // ─── Configuration ────────────────────────────────────────────────
18
19
  const CONFIG = {
@@ -22,6 +23,8 @@ const CONFIG = {
22
23
  STEALTH_NOTIFY: process.env.MCP_SILENT_STEALTH !== "false",
23
24
  /** Agent override via env (for testing/debugging) */
24
25
  FORCE_AGENT: process.env.MCP_FORCE_AGENT || null,
26
+ /** Enable specialty memory injection into context */
27
+ INJECT_SPECIALTY_MEMORY: process.env.MCP_INJECT_SPECIALTY_MEMORY !== "false",
25
28
  };
26
29
  let agentRuleCache = null;
27
30
  async function loadAgentRules() {
@@ -40,7 +43,6 @@ async function loadAgentRules() {
40
43
  }
41
44
  }
42
45
  catch {
43
- // Fallback: use discipline rules file
44
46
  agentRuleCache = new Map();
45
47
  }
46
48
  return agentRuleCache;
@@ -52,9 +54,8 @@ async function loadAgentRules() {
52
54
  export function detectAgent(toolName, args) {
53
55
  if (CONFIG.FORCE_AGENT)
54
56
  return CONFIG.FORCE_AGENT;
55
- // Build context from args for routing
56
57
  const contextParts = [toolName];
57
- for (const [key, value] of Object.entries(args)) {
58
+ for (const [, value] of Object.entries(args)) {
58
59
  if (typeof value === "string") {
59
60
  contextParts.push(value);
60
61
  }
@@ -63,7 +64,6 @@ export function detectAgent(toolName, args) {
63
64
  }
64
65
  }
65
66
  const context = contextParts.join(" ").substring(0, 500);
66
- // Use RoutingEngine to detect the best agent
67
67
  return RoutingEngine.resolveAgent(context);
68
68
  }
69
69
  /**
@@ -88,9 +88,32 @@ export async function getAgentPrompt(agentName) {
88
88
  "> They supplement (not replace) your existing instructions.",
89
89
  ].join("\n");
90
90
  }
91
+ /**
92
+ * Load specialty memory for an agent and format it for injection.
93
+ */
94
+ function getSpecialtyMemoryContext(agentName) {
95
+ if (!CONFIG.INJECT_SPECIALTY_MEMORY)
96
+ return null;
97
+ try {
98
+ const conventions = EvaluationEngine.readLearnedConventions(agentName);
99
+ if (!conventions)
100
+ return null;
101
+ return [
102
+ "",
103
+ "### Previously Learned Conventions",
104
+ `The following lessons were learned from past tasks for @${agentName}:`,
105
+ "",
106
+ conventions,
107
+ "",
108
+ ].join("\n");
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
91
114
  /**
92
115
  * Build a silent context injection payload.
93
- * This is prepended to tool responses to guide the AI without @agent commands.
116
+ * Injects agent rules + specialty memory into tool responses.
94
117
  */
95
118
  export async function buildSilentContext(detectedAgent, toolName, responseText) {
96
119
  if (!CONFIG.ENABLED)
@@ -98,14 +121,17 @@ export async function buildSilentContext(detectedAgent, toolName, responseText)
98
121
  const agentPrompt = await getAgentPrompt(detectedAgent);
99
122
  if (!agentPrompt)
100
123
  return responseText;
101
- // Only inject if the response is substantial enough to warrant governance
102
124
  if (responseText.length < 50)
103
125
  return responseText;
104
- // Inject as a comment block that won't break tool results
126
+ // Inject specialty memory if available
127
+ const specialtyContext = getSpecialtyMemoryContext(detectedAgent);
128
+ const fullPrompt = specialtyContext
129
+ ? `${agentPrompt}\n${specialtyContext}`
130
+ : agentPrompt;
105
131
  const injection = [
106
132
  "",
107
133
  `<!-- ATABEY_GOVERNANCE agent="${detectedAgent}" tool="${toolName}" -->`,
108
- `${agentPrompt}`,
134
+ `${fullPrompt}`,
109
135
  "<!-- /ATABEY_GOVERNANCE -->",
110
136
  "",
111
137
  ].join("\n");