@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,371 @@
1
+ /**
2
+ * OpenClaw Integration Hook
3
+ *
4
+ * This is the bridge between the enterprise engine and a running
5
+ * OpenClaw instance. It intercepts every tool call, checks permissions,
6
+ * records activity, injects knowledge base context, and enforces budgets.
7
+ *
8
+ * How it works:
9
+ * 1. OpenClaw plugin registers this hook on startup
10
+ * 2. Before every tool call → checkPermission + recordStart
11
+ * 3. After every tool call → recordEnd + update usage
12
+ * 4. Before every LLM call → inject KB context if relevant
13
+ * 5. On session start/end → lifecycle events
14
+ *
15
+ * Usage in OpenClaw plugin:
16
+ * import { createEnterpriseHook } from '@agenticmail/enterprise/hook';
17
+ * const hook = createEnterpriseHook({ engineUrl: 'http://localhost:4444' });
18
+ * // Register with OpenClaw's tool pipeline
19
+ */
20
+
21
+ // ─── Types ──────────────────────────────────────────────
22
+
23
+ export interface EnterpriseHookConfig {
24
+ /** URL of the enterprise engine API */
25
+ engineUrl: string;
26
+
27
+ /** Agent ID in the enterprise system */
28
+ agentId: string;
29
+
30
+ /** Organization ID */
31
+ orgId: string;
32
+
33
+ /** Auth token for engine API */
34
+ apiToken?: string;
35
+
36
+ /** Enable knowledge base context injection */
37
+ knowledgeBaseEnabled?: boolean;
38
+
39
+ /** Max tokens for KB context per turn */
40
+ kbMaxTokens?: number;
41
+
42
+ /** Enable real-time activity streaming */
43
+ activityStreamEnabled?: boolean;
44
+
45
+ /** Fail open (allow) or fail closed (deny) when engine is unreachable */
46
+ failMode?: 'open' | 'closed';
47
+
48
+ /** Cache permission checks for N seconds (reduces API calls) */
49
+ permissionCacheTtlSec?: number;
50
+ }
51
+
52
+ export interface ToolCallContext {
53
+ toolId: string;
54
+ toolName: string;
55
+ parameters: Record<string, any>;
56
+ sessionId: string;
57
+ timestamp?: Date;
58
+ }
59
+
60
+ export interface ToolCallResult {
61
+ success: boolean;
62
+ output?: string;
63
+ error?: string;
64
+ inputTokens?: number;
65
+ outputTokens?: number;
66
+ costUsd?: number;
67
+ }
68
+
69
+ export interface HookResult {
70
+ allowed: boolean;
71
+ reason: string;
72
+ requiresApproval: boolean;
73
+ approvalId?: string;
74
+ sandbox?: boolean;
75
+ /** Modified parameters (if hook wants to sanitize/restrict) */
76
+ modifiedParameters?: Record<string, any>;
77
+ }
78
+
79
+ // ─── Enterprise Hook ────────────────────────────────────
80
+
81
+ export class EnterpriseHook {
82
+ private config: Required<EnterpriseHookConfig>;
83
+ private permissionCache = new Map<string, { result: HookResult; expires: number }>();
84
+ private pendingToolCalls = new Map<string, string>(); // callId → toolCallRecordId
85
+ private connected = false;
86
+
87
+ constructor(config: EnterpriseHookConfig) {
88
+ this.config = {
89
+ engineUrl: config.engineUrl,
90
+ agentId: config.agentId,
91
+ orgId: config.orgId,
92
+ apiToken: config.apiToken || '',
93
+ knowledgeBaseEnabled: config.knowledgeBaseEnabled ?? true,
94
+ kbMaxTokens: config.kbMaxTokens ?? 2000,
95
+ activityStreamEnabled: config.activityStreamEnabled ?? true,
96
+ failMode: config.failMode ?? 'open',
97
+ permissionCacheTtlSec: config.permissionCacheTtlSec ?? 30,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * BEFORE a tool call — check permissions, record start
103
+ */
104
+ async beforeToolCall(ctx: ToolCallContext): Promise<HookResult> {
105
+ try {
106
+ // Check cache first
107
+ const cached = this.permissionCache.get(ctx.toolId);
108
+ if (cached && cached.expires > Date.now()) {
109
+ // Still record the call even if cached
110
+ await this.recordToolCallStart(ctx);
111
+ return cached.result;
112
+ }
113
+
114
+ // Check permission with engine
115
+ const permResult = await this.apiCall('/api/engine/permissions/check', 'POST', {
116
+ agentId: this.config.agentId,
117
+ toolId: ctx.toolId,
118
+ });
119
+
120
+ const result: HookResult = {
121
+ allowed: permResult.allowed ?? (this.config.failMode === 'open'),
122
+ reason: permResult.reason || 'Unknown',
123
+ requiresApproval: permResult.requiresApproval || false,
124
+ sandbox: permResult.sandbox || false,
125
+ };
126
+
127
+ // Cache the permission result
128
+ this.permissionCache.set(ctx.toolId, {
129
+ result,
130
+ expires: Date.now() + this.config.permissionCacheTtlSec * 1000,
131
+ });
132
+
133
+ // If approval required, create approval request and wait
134
+ if (result.requiresApproval && result.allowed) {
135
+ const approval = await this.requestApproval(ctx);
136
+ if (approval) {
137
+ result.approvalId = approval.id;
138
+ if (approval.status === 'denied') {
139
+ result.allowed = false;
140
+ result.reason = `Denied by ${approval.decision?.by}: ${approval.decision?.reason || 'No reason given'}`;
141
+ } else if (approval.status === 'expired') {
142
+ result.allowed = false;
143
+ result.reason = 'Approval request expired';
144
+ }
145
+ }
146
+ }
147
+
148
+ // Record the tool call start
149
+ if (result.allowed) {
150
+ await this.recordToolCallStart(ctx);
151
+ } else {
152
+ // Record blocked call
153
+ await this.recordActivity('tool_blocked', {
154
+ toolId: ctx.toolId,
155
+ toolName: ctx.toolName,
156
+ reason: result.reason,
157
+ });
158
+ }
159
+
160
+ return result;
161
+
162
+ } catch (error: any) {
163
+ // Engine unreachable — use fail mode
164
+ const allowed = this.config.failMode === 'open';
165
+ return {
166
+ allowed,
167
+ reason: allowed
168
+ ? `Engine unreachable (fail-open): ${error.message}`
169
+ : `Engine unreachable (fail-closed): ${error.message}`,
170
+ requiresApproval: false,
171
+ };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * AFTER a tool call — record result, update usage
177
+ */
178
+ async afterToolCall(ctx: ToolCallContext, result: ToolCallResult): Promise<void> {
179
+ try {
180
+ // Record usage with lifecycle manager
181
+ await this.apiCall('/api/engine/agents/' + this.config.agentId + '/record-tool-call', 'POST', {
182
+ toolId: ctx.toolId,
183
+ tokensUsed: (result.inputTokens || 0) + (result.outputTokens || 0),
184
+ costUsd: result.costUsd || 0,
185
+ isExternalAction: this.isExternalAction(ctx.toolId),
186
+ error: !result.success,
187
+ });
188
+
189
+ // Record activity event
190
+ await this.recordActivity(result.success ? 'tool_call_end' : 'tool_call_error', {
191
+ toolId: ctx.toolId,
192
+ toolName: ctx.toolName,
193
+ success: result.success,
194
+ error: result.error,
195
+ durationMs: ctx.timestamp ? Date.now() - ctx.timestamp.getTime() : undefined,
196
+ inputTokens: result.inputTokens,
197
+ outputTokens: result.outputTokens,
198
+ costUsd: result.costUsd,
199
+ });
200
+ } catch {
201
+ // Non-blocking — don't break the agent if tracking fails
202
+ }
203
+ }
204
+
205
+ /**
206
+ * BEFORE an LLM call — inject knowledge base context
207
+ */
208
+ async getKnowledgeContext(userMessage: string): Promise<string | null> {
209
+ if (!this.config.knowledgeBaseEnabled) return null;
210
+
211
+ try {
212
+ const result = await this.apiCall('/api/engine/knowledge-bases/context', 'POST', {
213
+ agentId: this.config.agentId,
214
+ query: userMessage,
215
+ maxTokens: this.config.kbMaxTokens,
216
+ });
217
+ return result.context || null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * ON session start
225
+ */
226
+ async onSessionStart(sessionId: string): Promise<void> {
227
+ try {
228
+ await this.recordActivity('session_start', { sessionId });
229
+ } catch { /* non-blocking */ }
230
+ }
231
+
232
+ /**
233
+ * ON session end
234
+ */
235
+ async onSessionEnd(sessionId: string): Promise<void> {
236
+ try {
237
+ await this.recordActivity('session_end', { sessionId });
238
+ } catch { /* non-blocking */ }
239
+ }
240
+
241
+ /**
242
+ * Record a conversation message
243
+ */
244
+ async recordMessage(opts: {
245
+ sessionId: string;
246
+ role: 'user' | 'assistant' | 'system';
247
+ content: string;
248
+ channel?: string;
249
+ tokenCount: number;
250
+ }): Promise<void> {
251
+ try {
252
+ await this.apiCall('/api/engine/activity/record-message', 'POST', {
253
+ agentId: this.config.agentId,
254
+ ...opts,
255
+ });
256
+ } catch { /* non-blocking */ }
257
+ }
258
+
259
+ /**
260
+ * Get the tool policy for this agent (used on startup to configure OpenClaw)
261
+ */
262
+ async getToolPolicy(): Promise<{
263
+ allowedTools: string[];
264
+ blockedTools: string[];
265
+ approvalRequired: string[];
266
+ rateLimits: any;
267
+ } | null> {
268
+ try {
269
+ return await this.apiCall(`/api/engine/permissions/${this.config.agentId}/policy`, 'GET');
270
+ } catch {
271
+ return null;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Check if engine is reachable
277
+ */
278
+ async healthCheck(): Promise<boolean> {
279
+ try {
280
+ const result = await this.apiCall('/health', 'GET');
281
+ this.connected = result.status === 'ok';
282
+ return this.connected;
283
+ } catch {
284
+ this.connected = false;
285
+ return false;
286
+ }
287
+ }
288
+
289
+ // ─── Private ──────────────────────────────────────────
290
+
291
+ private async requestApproval(ctx: ToolCallContext): Promise<any> {
292
+ try {
293
+ const result = await this.apiCall('/api/engine/approvals/request', 'POST', {
294
+ agentId: this.config.agentId,
295
+ agentName: this.config.agentId,
296
+ toolId: ctx.toolId,
297
+ toolName: ctx.toolName,
298
+ parameters: ctx.parameters,
299
+ context: `Session ${ctx.sessionId}`,
300
+ });
301
+
302
+ if (result.request?.id) {
303
+ // Wait for decision (up to 5 minutes)
304
+ const start = Date.now();
305
+ while (Date.now() - start < 300_000) {
306
+ await new Promise(r => setTimeout(r, 3000));
307
+ const check = await this.apiCall(`/api/engine/approvals/${result.request.id}`, 'GET');
308
+ if (check.request?.status !== 'pending') return check.request;
309
+ }
310
+ return { status: 'expired' };
311
+ }
312
+ return null;
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ private async recordToolCallStart(ctx: ToolCallContext): Promise<void> {
319
+ if (!this.config.activityStreamEnabled) return;
320
+ try {
321
+ await this.recordActivity('tool_call_start', {
322
+ toolId: ctx.toolId,
323
+ toolName: ctx.toolName,
324
+ sessionId: ctx.sessionId,
325
+ });
326
+ } catch { /* non-blocking */ }
327
+ }
328
+
329
+ private async recordActivity(type: string, data: Record<string, any>): Promise<void> {
330
+ try {
331
+ await this.apiCall('/api/engine/activity/record', 'POST', {
332
+ agentId: this.config.agentId,
333
+ orgId: this.config.orgId,
334
+ type,
335
+ data,
336
+ });
337
+ } catch { /* non-blocking */ }
338
+ }
339
+
340
+ private isExternalAction(toolId: string): boolean {
341
+ const externalTools = [
342
+ 'agenticmail_send', 'agenticmail_reply', 'agenticmail_forward',
343
+ 'agenticmail_sms_send', 'message', 'tts',
344
+ ];
345
+ return externalTools.includes(toolId);
346
+ }
347
+
348
+ private async apiCall(path: string, method: string, body?: any): Promise<any> {
349
+ const opts: RequestInit = {
350
+ method,
351
+ headers: {
352
+ 'Content-Type': 'application/json',
353
+ ...(this.config.apiToken ? { 'Authorization': `Bearer ${this.config.apiToken}` } : {}),
354
+ },
355
+ signal: AbortSignal.timeout(5000),
356
+ };
357
+ if (body && method !== 'GET') opts.body = JSON.stringify(body);
358
+
359
+ const resp = await fetch(`${this.config.engineUrl}${path}`, opts);
360
+ return resp.json();
361
+ }
362
+ }
363
+
364
+ // ─── Factory ────────────────────────────────────────────
365
+
366
+ /**
367
+ * Create an enterprise hook instance for use in OpenClaw plugins
368
+ */
369
+ export function createEnterpriseHook(config: EnterpriseHookConfig): EnterpriseHook {
370
+ return new EnterpriseHook(config);
371
+ }