@agenticmail/enterprise 0.2.1

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 (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. package/tsconfig.json +14 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * AgenticMail Bridge
3
+ *
4
+ * Integrates the enterprise engine with AgenticMail's OpenClaw plugin.
5
+ * This is the glue code that makes enterprise features available
6
+ * inside an agent's OpenClaw session.
7
+ *
8
+ * When an enterprise-managed agent boots:
9
+ * 1. Bridge loads agent config from engine
10
+ * 2. Registers permission middleware on every tool call
11
+ * 3. Injects KB context before LLM calls
12
+ * 4. Streams activity events to the engine
13
+ * 5. Enforces rate limits and budgets
14
+ *
15
+ * Usage:
16
+ * // In OpenClaw plugin init
17
+ * import { createAgenticMailBridge } from '@agenticmail/enterprise/bridge';
18
+ * const bridge = await createAgenticMailBridge({
19
+ * engineUrl: process.env.AGENTICMAIL_ENTERPRISE_URL,
20
+ * agentId: process.env.AGENTICMAIL_AGENT_ID,
21
+ * orgId: process.env.AGENTICMAIL_ORG_ID,
22
+ * });
23
+ * bridge.install(openclawPlugin);
24
+ */
25
+
26
+ import { EnterpriseHook, type EnterpriseHookConfig } from './openclaw-hook.js';
27
+ import { TOOL_INDEX, generateOpenClawToolPolicy } from './tool-catalog.js';
28
+
29
+ // ─── Types ──────────────────────────────────────────────
30
+
31
+ export interface BridgeConfig extends EnterpriseHookConfig {
32
+ /** Auto-configure tool policy on startup */
33
+ autoConfigureTools?: boolean;
34
+
35
+ /** Inject coordination context about enterprise features */
36
+ injectCoordinationContext?: boolean;
37
+
38
+ /** Log enterprise events to console */
39
+ verbose?: boolean;
40
+ }
41
+
42
+ export interface ToolInterceptor {
43
+ /** Called before every tool invocation. Return false to block. */
44
+ beforeTool: (toolId: string, params: Record<string, any>, sessionId: string) => Promise<{
45
+ allowed: boolean;
46
+ reason?: string;
47
+ modifiedParams?: Record<string, any>;
48
+ }>;
49
+
50
+ /** Called after every tool invocation */
51
+ afterTool: (toolId: string, params: Record<string, any>, result: any, sessionId: string) => Promise<void>;
52
+ }
53
+
54
+ // ─── Bridge ─────────────────────────────────────────────
55
+
56
+ export class AgenticMailBridge {
57
+ private hook: EnterpriseHook;
58
+ private config: BridgeConfig;
59
+ private toolPolicy: { allowedTools: string[]; blockedTools: string[]; approvalRequired: string[] } | null = null;
60
+ private sessionId: string = '';
61
+ private rateLimiter: { count: number; resetAt: number } = { count: 0, resetAt: 0 };
62
+
63
+ constructor(config: BridgeConfig) {
64
+ this.config = config;
65
+ this.hook = new EnterpriseHook(config);
66
+ }
67
+
68
+ /**
69
+ * Initialize the bridge — load config, verify connection
70
+ */
71
+ async initialize(): Promise<{ connected: boolean; toolPolicy: any }> {
72
+ const connected = await this.hook.healthCheck();
73
+
74
+ if (connected && this.config.autoConfigureTools !== false) {
75
+ this.toolPolicy = await this.hook.getToolPolicy();
76
+ }
77
+
78
+ if (this.config.verbose) {
79
+ console.log(`[Enterprise] Bridge initialized. Connected: ${connected}`);
80
+ if (this.toolPolicy) {
81
+ console.log(`[Enterprise] Tool policy: ${this.toolPolicy.allowedTools.length} allowed, ${this.toolPolicy.blockedTools.length} blocked`);
82
+ }
83
+ }
84
+
85
+ return { connected, toolPolicy: this.toolPolicy };
86
+ }
87
+
88
+ /**
89
+ * Get a tool interceptor that can be registered with OpenClaw
90
+ */
91
+ getInterceptor(): ToolInterceptor {
92
+ return {
93
+ beforeTool: async (toolId, params, sessionId) => {
94
+ this.sessionId = sessionId;
95
+
96
+ // Quick local check first (no API call)
97
+ if (this.toolPolicy?.blockedTools.includes(toolId)) {
98
+ if (this.config.verbose) console.log(`[Enterprise] BLOCKED: ${toolId} (policy)`);
99
+ return { allowed: false, reason: `Tool "${toolId}" is blocked by enterprise policy` };
100
+ }
101
+
102
+ // Rate limit check
103
+ const now = Date.now();
104
+ if (now > this.rateLimiter.resetAt) {
105
+ this.rateLimiter = { count: 0, resetAt: now + 60_000 };
106
+ }
107
+ this.rateLimiter.count++;
108
+ // Default 120/min, will be overridden by policy
109
+ const limit = (this.toolPolicy as any)?.rateLimits?.toolCallsPerMinute || 120;
110
+ if (this.rateLimiter.count > limit) {
111
+ return { allowed: false, reason: `Rate limit exceeded: ${this.rateLimiter.count}/${limit} calls/min` };
112
+ }
113
+
114
+ // Full permission check via engine API
115
+ const result = await this.hook.beforeToolCall({
116
+ toolId, toolName: toolId, parameters: params, sessionId,
117
+ timestamp: new Date(),
118
+ });
119
+
120
+ if (this.config.verbose && !result.allowed) {
121
+ console.log(`[Enterprise] BLOCKED: ${toolId} — ${result.reason}`);
122
+ }
123
+
124
+ return {
125
+ allowed: result.allowed,
126
+ reason: result.reason,
127
+ modifiedParams: result.modifiedParameters,
128
+ };
129
+ },
130
+
131
+ afterTool: async (toolId, params, result, sessionId) => {
132
+ await this.hook.afterToolCall(
133
+ { toolId, toolName: toolId, parameters: params, sessionId, timestamp: new Date() },
134
+ {
135
+ success: !result?.error,
136
+ output: typeof result === 'string' ? result : JSON.stringify(result)?.slice(0, 500),
137
+ error: result?.error,
138
+ },
139
+ );
140
+ },
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get knowledge base context to inject before LLM call
146
+ */
147
+ async getKBContext(userMessage: string): Promise<string | null> {
148
+ return this.hook.getKnowledgeContext(userMessage);
149
+ }
150
+
151
+ /**
152
+ * Generate coordination context for the agent's system prompt
153
+ */
154
+ getCoordinationContext(): string {
155
+ if (!this.config.injectCoordinationContext) return '';
156
+
157
+ const blocked = this.toolPolicy?.blockedTools || [];
158
+ const needsApproval = this.toolPolicy?.approvalRequired || [];
159
+
160
+ let ctx = '\n<enterprise-context>\n';
161
+ ctx += '🏢 This agent is managed by AgenticMail Enterprise.\n';
162
+
163
+ if (blocked.length > 0) {
164
+ ctx += `⛔ Blocked tools (do not attempt): ${blocked.join(', ')}\n`;
165
+ }
166
+ if (needsApproval.length > 0) {
167
+ ctx += `⚠️ Tools requiring human approval: ${needsApproval.join(', ')}\n`;
168
+ ctx += 'When using these tools, the call will pause until a human approves or denies.\n';
169
+ }
170
+
171
+ ctx += '</enterprise-context>\n';
172
+ return ctx;
173
+ }
174
+
175
+ /**
176
+ * Notify engine of session lifecycle
177
+ */
178
+ async onSessionStart(sessionId: string): Promise<void> {
179
+ this.sessionId = sessionId;
180
+ await this.hook.onSessionStart(sessionId);
181
+ }
182
+
183
+ async onSessionEnd(sessionId: string): Promise<void> {
184
+ await this.hook.onSessionEnd(sessionId);
185
+ }
186
+
187
+ /**
188
+ * Record a message in the conversation log
189
+ */
190
+ async recordMessage(role: 'user' | 'assistant' | 'system', content: string, opts?: {
191
+ channel?: string;
192
+ tokenCount?: number;
193
+ }): Promise<void> {
194
+ await this.hook.recordMessage({
195
+ sessionId: this.sessionId,
196
+ role,
197
+ content,
198
+ channel: opts?.channel,
199
+ tokenCount: opts?.tokenCount || Math.ceil(content.length / 4),
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Get the loaded tool policy
205
+ */
206
+ getToolPolicy() {
207
+ return this.toolPolicy;
208
+ }
209
+
210
+ /**
211
+ * Generate OpenClaw-compatible config for tools.allow / tools.deny
212
+ * This is what gets written to the gateway config
213
+ */
214
+ getOpenClawToolConfig(): Record<string, any> {
215
+ if (!this.toolPolicy) return {};
216
+ return generateOpenClawToolPolicy(
217
+ this.toolPolicy.allowedTools,
218
+ this.toolPolicy.blockedTools,
219
+ );
220
+ }
221
+ }
222
+
223
+ // ─── Factory ────────────────────────────────────────────
224
+
225
+ /**
226
+ * Create and initialize the AgenticMail Enterprise bridge
227
+ */
228
+ export async function createAgenticMailBridge(config: BridgeConfig): Promise<AgenticMailBridge> {
229
+ const bridge = new AgenticMailBridge({
230
+ autoConfigureTools: true,
231
+ injectCoordinationContext: true,
232
+ verbose: false,
233
+ failMode: 'open',
234
+ permissionCacheTtlSec: 30,
235
+ kbMaxTokens: 2000,
236
+ knowledgeBaseEnabled: true,
237
+ activityStreamEnabled: true,
238
+ ...config,
239
+ });
240
+
241
+ await bridge.initialize();
242
+ return bridge;
243
+ }
244
+
245
+ /**
246
+ * Example integration with OpenClaw plugin:
247
+ *
248
+ * ```typescript
249
+ * // In @agenticmail/openclaw plugin init
250
+ * import { createAgenticMailBridge } from '@agenticmail/enterprise/bridge';
251
+ *
252
+ * export async function initPlugin(openclaw) {
253
+ * // Check if enterprise mode is enabled
254
+ * const engineUrl = process.env.AGENTICMAIL_ENTERPRISE_URL;
255
+ * if (!engineUrl) return; // Not enterprise, skip
256
+ *
257
+ * const bridge = await createAgenticMailBridge({
258
+ * engineUrl,
259
+ * agentId: process.env.AGENTICMAIL_AGENT_ID,
260
+ * orgId: process.env.AGENTICMAIL_ORG_ID,
261
+ * apiToken: process.env.AGENTICMAIL_ENTERPRISE_TOKEN,
262
+ * });
263
+ *
264
+ * // Register tool interceptor
265
+ * const interceptor = bridge.getInterceptor();
266
+ * openclaw.onBeforeToolCall(async (tool, params, session) => {
267
+ * const result = await interceptor.beforeTool(tool, params, session.id);
268
+ * if (!result.allowed) throw new Error(`Enterprise: ${result.reason}`);
269
+ * return result.modifiedParams || params;
270
+ * });
271
+ *
272
+ * openclaw.onAfterToolCall(async (tool, params, result, session) => {
273
+ * await interceptor.afterTool(tool, params, result, session.id);
274
+ * });
275
+ *
276
+ * // Inject KB context before LLM
277
+ * openclaw.onBeforeLLM(async (messages) => {
278
+ * const lastUserMsg = messages.findLast(m => m.role === 'user');
279
+ * if (lastUserMsg) {
280
+ * const kbContext = await bridge.getKBContext(lastUserMsg.content);
281
+ * if (kbContext) {
282
+ * messages.push({ role: 'system', content: kbContext });
283
+ * }
284
+ * }
285
+ * return messages;
286
+ * });
287
+ *
288
+ * // Add coordination context to system prompt
289
+ * openclaw.appendSystemPrompt(bridge.getCoordinationContext());
290
+ *
291
+ * // Session lifecycle
292
+ * openclaw.onSessionStart((s) => bridge.onSessionStart(s.id));
293
+ * openclaw.onSessionEnd((s) => bridge.onSessionEnd(s.id));
294
+ * }
295
+ * ```
296
+ */
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Approval Workflow Engine
3
+ *
4
+ * Human-in-the-loop for sensitive agent operations.
5
+ * When a tool requires approval (based on risk level or side effects),
6
+ * the engine queues the request and notifies approvers.
7
+ */
8
+
9
+ // ─── Types ──────────────────────────────────────────────
10
+
11
+ export interface ApprovalRequest {
12
+ id: string;
13
+ agentId: string;
14
+ agentName: string;
15
+ toolId: string;
16
+ toolName: string;
17
+ reason: string; // Why approval is needed
18
+ riskLevel: string;
19
+ sideEffects: string[];
20
+ parameters?: Record<string, any>; // What the agent wants to do (sanitized)
21
+ context?: string; // Brief description of what the agent is working on
22
+ status: 'pending' | 'approved' | 'denied' | 'expired';
23
+ decision?: ApprovalDecision;
24
+ createdAt: string;
25
+ expiresAt: string; // Auto-deny after this
26
+ }
27
+
28
+ export interface ApprovalDecision {
29
+ by: string; // User ID of approver
30
+ action: 'approve' | 'deny';
31
+ reason?: string; // Optional reason for deny
32
+ timestamp: string;
33
+ conditions?: string; // "Approved with conditions: ..."
34
+ }
35
+
36
+ export interface ApprovalPolicy {
37
+ id: string;
38
+ name: string;
39
+ description?: string;
40
+
41
+ // What triggers this policy
42
+ triggers: {
43
+ riskLevels?: string[];
44
+ sideEffects?: string[];
45
+ toolIds?: string[]; // Specific tools
46
+ allExternalActions?: boolean; // Any action with side effects
47
+ };
48
+
49
+ // Who can approve
50
+ approvers: {
51
+ userIds: string[]; // Specific users
52
+ roles: string[]; // Any user with these roles
53
+ requireMultiple?: number; // Require N approvals (default: 1)
54
+ };
55
+
56
+ // Timing
57
+ timeout: {
58
+ minutes: number;
59
+ defaultAction: 'deny' | 'allow'; // What happens on timeout
60
+ };
61
+
62
+ // Notification
63
+ notify: {
64
+ channels: ('email' | 'slack' | 'webhook')[];
65
+ webhookUrl?: string;
66
+ slackChannel?: string;
67
+ };
68
+
69
+ enabled: boolean;
70
+ }
71
+
72
+ // ─── Engine ─────────────────────────────────────────────
73
+
74
+ export class ApprovalEngine {
75
+ private requests = new Map<string, ApprovalRequest>();
76
+ private policies: ApprovalPolicy[] = [];
77
+ private listeners: ((req: ApprovalRequest) => void)[] = [];
78
+
79
+ addPolicy(policy: ApprovalPolicy) {
80
+ this.policies.push(policy);
81
+ }
82
+
83
+ removePolicy(id: string) {
84
+ this.policies = this.policies.filter(p => p.id !== id);
85
+ }
86
+
87
+ getPolicies(): ApprovalPolicy[] {
88
+ return [...this.policies];
89
+ }
90
+
91
+ /**
92
+ * Check if a tool call needs approval and create a request if so
93
+ */
94
+ async requestApproval(opts: {
95
+ agentId: string;
96
+ agentName: string;
97
+ toolId: string;
98
+ toolName: string;
99
+ riskLevel: string;
100
+ sideEffects: string[];
101
+ parameters?: Record<string, any>;
102
+ context?: string;
103
+ }): Promise<ApprovalRequest | null> {
104
+ // Find matching policy
105
+ const policy = this.findMatchingPolicy(opts.toolId, opts.riskLevel, opts.sideEffects);
106
+ if (!policy) return null; // No approval needed
107
+
108
+ const request: ApprovalRequest = {
109
+ id: crypto.randomUUID(),
110
+ agentId: opts.agentId,
111
+ agentName: opts.agentName,
112
+ toolId: opts.toolId,
113
+ toolName: opts.toolName,
114
+ reason: `Policy "${policy.name}" requires approval`,
115
+ riskLevel: opts.riskLevel,
116
+ sideEffects: opts.sideEffects,
117
+ parameters: this.sanitizeParams(opts.parameters),
118
+ context: opts.context,
119
+ status: 'pending',
120
+ createdAt: new Date().toISOString(),
121
+ expiresAt: new Date(Date.now() + policy.timeout.minutes * 60_000).toISOString(),
122
+ };
123
+
124
+ this.requests.set(request.id, request);
125
+
126
+ // Notify approvers
127
+ await this.notifyApprovers(request, policy);
128
+
129
+ // Set expiry timer
130
+ setTimeout(() => {
131
+ const req = this.requests.get(request.id);
132
+ if (req && req.status === 'pending') {
133
+ req.status = 'expired';
134
+ if (policy.timeout.defaultAction === 'allow') {
135
+ req.status = 'approved';
136
+ req.decision = {
137
+ by: 'system',
138
+ action: 'approve',
139
+ reason: 'Auto-approved: approval timeout expired',
140
+ timestamp: new Date().toISOString(),
141
+ };
142
+ }
143
+ this.notifyListeners(req);
144
+ }
145
+ }, policy.timeout.minutes * 60_000);
146
+
147
+ // Notify listeners
148
+ this.notifyListeners(request);
149
+
150
+ return request;
151
+ }
152
+
153
+ /**
154
+ * Approve or deny a pending request
155
+ */
156
+ decide(requestId: string, decision: Omit<ApprovalDecision, 'timestamp'>): ApprovalRequest | null {
157
+ const request = this.requests.get(requestId);
158
+ if (!request || request.status !== 'pending') return null;
159
+
160
+ request.status = decision.action === 'approve' ? 'approved' : 'denied';
161
+ request.decision = { ...decision, timestamp: new Date().toISOString() };
162
+
163
+ this.notifyListeners(request);
164
+ return request;
165
+ }
166
+
167
+ /**
168
+ * Get all pending requests (for the dashboard)
169
+ */
170
+ getPendingRequests(agentId?: string): ApprovalRequest[] {
171
+ const all = Array.from(this.requests.values()).filter(r => r.status === 'pending');
172
+ return agentId ? all.filter(r => r.agentId === agentId) : all;
173
+ }
174
+
175
+ /**
176
+ * Get request by ID
177
+ */
178
+ getRequest(id: string): ApprovalRequest | undefined {
179
+ return this.requests.get(id);
180
+ }
181
+
182
+ /**
183
+ * Get history of all requests
184
+ */
185
+ getHistory(opts?: { agentId?: string; limit?: number; offset?: number }): { requests: ApprovalRequest[]; total: number } {
186
+ let all = Array.from(this.requests.values()).sort((a, b) =>
187
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
188
+ );
189
+ if (opts?.agentId) all = all.filter(r => r.agentId === opts.agentId);
190
+ const total = all.length;
191
+ const offset = opts?.offset || 0;
192
+ const limit = opts?.limit || 25;
193
+ return { requests: all.slice(offset, offset + limit), total };
194
+ }
195
+
196
+ /**
197
+ * Wait for a specific request to be decided (for sync approval flows)
198
+ */
199
+ async waitForDecision(requestId: string, timeoutMs: number = 300_000): Promise<ApprovalRequest> {
200
+ return new Promise((resolve, reject) => {
201
+ const check = setInterval(() => {
202
+ const req = this.requests.get(requestId);
203
+ if (req && req.status !== 'pending') {
204
+ clearInterval(check);
205
+ clearTimeout(timeout);
206
+ resolve(req);
207
+ }
208
+ }, 1000);
209
+
210
+ const timeout = setTimeout(() => {
211
+ clearInterval(check);
212
+ const req = this.requests.get(requestId);
213
+ if (req) {
214
+ req.status = 'expired';
215
+ resolve(req);
216
+ } else {
217
+ reject(new Error('Request not found'));
218
+ }
219
+ }, timeoutMs);
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Subscribe to approval request changes
225
+ */
226
+ onRequest(listener: (req: ApprovalRequest) => void) {
227
+ this.listeners.push(listener);
228
+ return () => { this.listeners = this.listeners.filter(l => l !== listener); };
229
+ }
230
+
231
+ // ─── Private ──────────────────────────────────────────
232
+
233
+ private findMatchingPolicy(toolId: string, riskLevel: string, sideEffects: string[]): ApprovalPolicy | undefined {
234
+ return this.policies.find(p => {
235
+ if (!p.enabled) return false;
236
+ if (p.triggers.toolIds?.includes(toolId)) return true;
237
+ if (p.triggers.riskLevels?.includes(riskLevel)) return true;
238
+ if (p.triggers.sideEffects?.some(e => sideEffects.includes(e))) return true;
239
+ if (p.triggers.allExternalActions && sideEffects.length > 0) return true;
240
+ return false;
241
+ });
242
+ }
243
+
244
+ private sanitizeParams(params?: Record<string, any>): Record<string, any> | undefined {
245
+ if (!params) return undefined;
246
+ // Remove sensitive fields
247
+ const sanitized = { ...params };
248
+ for (const key of ['password', 'token', 'secret', 'key', 'apiKey', 'credential']) {
249
+ if (key in sanitized) sanitized[key] = '***';
250
+ }
251
+ return sanitized;
252
+ }
253
+
254
+ private async notifyApprovers(request: ApprovalRequest, policy: ApprovalPolicy) {
255
+ for (const channel of policy.notify.channels) {
256
+ switch (channel) {
257
+ case 'webhook':
258
+ if (policy.notify.webhookUrl) {
259
+ try {
260
+ await fetch(policy.notify.webhookUrl, {
261
+ method: 'POST',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify({ type: 'approval_request', request }),
264
+ });
265
+ } catch { /* fail silently */ }
266
+ }
267
+ break;
268
+ // Email and Slack notifications would integrate with the existing AgenticMail system
269
+ }
270
+ }
271
+ }
272
+
273
+ private notifyListeners(request: ApprovalRequest) {
274
+ for (const listener of this.listeners) {
275
+ try { listener(request); } catch { /* ignore */ }
276
+ }
277
+ }
278
+ }