@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.
- package/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- 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
|
+
}
|