@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,2553 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__esm,
|
|
3
|
+
__export,
|
|
4
|
+
__toCommonJS
|
|
5
|
+
} from "./chunk-PNKVD2UK.js";
|
|
6
|
+
|
|
7
|
+
// src/engine/tool-catalog.ts
|
|
8
|
+
var tool_catalog_exports = {};
|
|
9
|
+
__export(tool_catalog_exports, {
|
|
10
|
+
AGENTICMAIL_TOOLS: () => AGENTICMAIL_TOOLS,
|
|
11
|
+
ALL_TOOLS: () => ALL_TOOLS,
|
|
12
|
+
OPENCLAW_CORE_TOOLS: () => OPENCLAW_CORE_TOOLS,
|
|
13
|
+
TOOL_INDEX: () => TOOL_INDEX,
|
|
14
|
+
generateOpenClawToolPolicy: () => generateOpenClawToolPolicy,
|
|
15
|
+
getToolsBySkill: () => getToolsBySkill
|
|
16
|
+
});
|
|
17
|
+
function getToolsBySkill() {
|
|
18
|
+
const map = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const tool of ALL_TOOLS) {
|
|
20
|
+
const list = map.get(tool.skillId) || [];
|
|
21
|
+
list.push(tool.id);
|
|
22
|
+
map.set(tool.skillId, list);
|
|
23
|
+
}
|
|
24
|
+
return map;
|
|
25
|
+
}
|
|
26
|
+
function generateOpenClawToolPolicy(allowedToolIds, blockedToolIds) {
|
|
27
|
+
const config = {};
|
|
28
|
+
if (blockedToolIds.length > 0 && allowedToolIds.length === 0) {
|
|
29
|
+
config["tools.deny"] = blockedToolIds;
|
|
30
|
+
} else if (allowedToolIds.length > 0) {
|
|
31
|
+
config["tools.allow"] = allowedToolIds;
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
var OPENCLAW_CORE_TOOLS, AGENTICMAIL_TOOLS, ALL_TOOLS, TOOL_INDEX;
|
|
36
|
+
var init_tool_catalog = __esm({
|
|
37
|
+
"src/engine/tool-catalog.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
OPENCLAW_CORE_TOOLS = [
|
|
40
|
+
// File system
|
|
41
|
+
{ id: "read", name: "Read File", description: "Read file contents (text and images)", category: "read", risk: "low", skillId: "files", sideEffects: [] },
|
|
42
|
+
{ id: "write", name: "Write File", description: "Create or overwrite files", category: "write", risk: "medium", skillId: "files", sideEffects: ["modifies-files"] },
|
|
43
|
+
{ id: "edit", name: "Edit File", description: "Make precise edits to files", category: "write", risk: "medium", skillId: "files", sideEffects: ["modifies-files"] },
|
|
44
|
+
// Execution
|
|
45
|
+
{ id: "exec", name: "Shell Command", description: "Execute shell commands", category: "execute", risk: "critical", skillId: "exec", sideEffects: ["runs-code", "modifies-files", "network-request"] },
|
|
46
|
+
{ id: "process", name: "Process Manager", description: "Manage running exec sessions", category: "execute", risk: "high", skillId: "exec", sideEffects: ["runs-code"] },
|
|
47
|
+
// Web
|
|
48
|
+
{ id: "web_search", name: "Web Search", description: "Search the web via Brave Search API", category: "read", risk: "low", skillId: "web-search", sideEffects: ["network-request"] },
|
|
49
|
+
{ id: "web_fetch", name: "Web Fetch", description: "Fetch and extract content from URLs", category: "read", risk: "low", skillId: "web-fetch", sideEffects: ["network-request"] },
|
|
50
|
+
// Browser
|
|
51
|
+
{ id: "browser", name: "Browser Control", description: "Automate web browsers", category: "execute", risk: "high", skillId: "browser", sideEffects: ["network-request", "runs-code"] },
|
|
52
|
+
// Canvas
|
|
53
|
+
{ id: "canvas", name: "Canvas", description: "Present/eval/snapshot rendered UI", category: "read", risk: "low", skillId: "canvas", sideEffects: [] },
|
|
54
|
+
// Nodes
|
|
55
|
+
{ id: "nodes", name: "Node Control", description: "Control paired devices", category: "execute", risk: "high", skillId: "nodes", sideEffects: ["controls-device"] },
|
|
56
|
+
// Cron
|
|
57
|
+
{ id: "cron", name: "Cron Jobs", description: "Schedule tasks and reminders", category: "write", risk: "medium", skillId: "cron", sideEffects: [] },
|
|
58
|
+
// Messaging
|
|
59
|
+
{ id: "message", name: "Send Message", description: "Send messages via channels", category: "communicate", risk: "high", skillId: "messaging", sideEffects: ["sends-message"] },
|
|
60
|
+
// Gateway
|
|
61
|
+
{ id: "gateway", name: "Gateway Control", description: "Restart, configure, update OpenClaw", category: "execute", risk: "critical", skillId: "gateway", sideEffects: ["runs-code"] },
|
|
62
|
+
// Sessions / Sub-agents
|
|
63
|
+
{ id: "agents_list", name: "List Agents", description: "List agent IDs for spawning", category: "read", risk: "low", skillId: "sessions", sideEffects: [] },
|
|
64
|
+
{ id: "sessions_list", name: "List Sessions", description: "List active sessions", category: "read", risk: "low", skillId: "sessions", sideEffects: [] },
|
|
65
|
+
{ id: "sessions_history", name: "Session History", description: "Fetch message history", category: "read", risk: "low", skillId: "sessions", sideEffects: [] },
|
|
66
|
+
{ id: "sessions_send", name: "Send to Session", description: "Send message to another session", category: "communicate", risk: "medium", skillId: "sessions", sideEffects: ["sends-message"] },
|
|
67
|
+
{ id: "sessions_spawn", name: "Spawn Sub-Agent", description: "Spawn a background sub-agent", category: "execute", risk: "medium", skillId: "sessions", sideEffects: [] },
|
|
68
|
+
{ id: "subagents", name: "Sub-Agent Control", description: "List, steer, or kill sub-agents", category: "execute", risk: "medium", skillId: "sessions", sideEffects: [] },
|
|
69
|
+
{ id: "session_status", name: "Session Status", description: "Show session usage and status", category: "read", risk: "low", skillId: "sessions", sideEffects: [] },
|
|
70
|
+
// Image
|
|
71
|
+
{ id: "image", name: "Image Analysis", description: "Analyze images with vision model", category: "read", risk: "low", skillId: "media", sideEffects: [] },
|
|
72
|
+
// TTS
|
|
73
|
+
{ id: "tts", name: "Text-to-Speech", description: "Convert text to speech audio", category: "write", risk: "low", skillId: "media", sideEffects: [] },
|
|
74
|
+
// Memory
|
|
75
|
+
{ id: "memory_search", name: "Memory Search", description: "Search agent memory files", category: "read", risk: "low", skillId: "memory", sideEffects: [] },
|
|
76
|
+
{ id: "memory_get", name: "Memory Get", description: "Read memory file snippets", category: "read", risk: "low", skillId: "memory", sideEffects: [] }
|
|
77
|
+
];
|
|
78
|
+
AGENTICMAIL_TOOLS = [
|
|
79
|
+
// Core email
|
|
80
|
+
{ id: "agenticmail_inbox", name: "Inbox", description: "List recent emails", category: "read", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
81
|
+
{ id: "agenticmail_read", name: "Read Email", description: "Read a specific email by UID", category: "read", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
82
|
+
{ id: "agenticmail_send", name: "Send Email", description: "Send an email", category: "communicate", risk: "high", skillId: "agenticmail", sideEffects: ["sends-email"] },
|
|
83
|
+
{ id: "agenticmail_reply", name: "Reply to Email", description: "Reply to an email", category: "communicate", risk: "high", skillId: "agenticmail", sideEffects: ["sends-email"] },
|
|
84
|
+
{ id: "agenticmail_forward", name: "Forward Email", description: "Forward an email", category: "communicate", risk: "high", skillId: "agenticmail", sideEffects: ["sends-email"] },
|
|
85
|
+
{ id: "agenticmail_search", name: "Search Email", description: "Search emails", category: "read", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
86
|
+
{ id: "agenticmail_delete", name: "Delete Email", description: "Delete an email", category: "destroy", risk: "medium", skillId: "agenticmail", sideEffects: ["deletes-data"] },
|
|
87
|
+
{ id: "agenticmail_move", name: "Move Email", description: "Move email to folder", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
88
|
+
{ id: "agenticmail_mark_read", name: "Mark Read", description: "Mark email as read", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
89
|
+
{ id: "agenticmail_mark_unread", name: "Mark Unread", description: "Mark email as unread", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
90
|
+
{ id: "agenticmail_digest", name: "Inbox Digest", description: "Get compact inbox digest", category: "read", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
91
|
+
{ id: "agenticmail_list_folder", name: "List Folder", description: "List messages in a folder", category: "read", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
92
|
+
{ id: "agenticmail_folders", name: "List Folders", description: "List all mail folders", category: "read", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
93
|
+
{ id: "agenticmail_create_folder", name: "Create Folder", description: "Create a mail folder", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
94
|
+
{ id: "agenticmail_import_relay", name: "Import Relay", description: "Import email from Gmail/Outlook", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
95
|
+
// Batch operations
|
|
96
|
+
{ id: "agenticmail_batch_read", name: "Batch Read", description: "Read multiple emails", category: "read", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
97
|
+
{ id: "agenticmail_batch_delete", name: "Batch Delete", description: "Delete multiple emails", category: "destroy", risk: "medium", skillId: "agenticmail", sideEffects: ["deletes-data"] },
|
|
98
|
+
{ id: "agenticmail_batch_move", name: "Batch Move", description: "Move multiple emails", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
99
|
+
{ id: "agenticmail_batch_mark_read", name: "Batch Mark Read", description: "Mark multiple as read", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
100
|
+
{ id: "agenticmail_batch_mark_unread", name: "Batch Mark Unread", description: "Mark multiple as unread", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
101
|
+
// Agent coordination
|
|
102
|
+
{ id: "agenticmail_call_agent", name: "Call Agent", description: "Call another agent with a task", category: "execute", risk: "medium", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
103
|
+
{ id: "agenticmail_message_agent", name: "Message Agent", description: "Send message to another agent", category: "communicate", risk: "low", skillId: "agenticmail-coordination", sideEffects: ["sends-message"] },
|
|
104
|
+
{ id: "agenticmail_list_agents", name: "List Agents", description: "List available agents", category: "read", risk: "low", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
105
|
+
{ id: "agenticmail_check_messages", name: "Check Messages", description: "Check for unread messages", category: "read", risk: "low", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
106
|
+
{ id: "agenticmail_check_tasks", name: "Check Tasks", description: "Check pending tasks", category: "read", risk: "low", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
107
|
+
{ id: "agenticmail_claim_task", name: "Claim Task", description: "Claim a pending task", category: "write", risk: "low", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
108
|
+
{ id: "agenticmail_complete_task", name: "Complete Task", description: "Complete a claimed task", category: "write", risk: "low", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
109
|
+
{ id: "agenticmail_submit_result", name: "Submit Result", description: "Submit task result", category: "write", risk: "low", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
110
|
+
{ id: "agenticmail_wait_for_email", name: "Wait for Email", description: "Wait for new email (SSE)", category: "read", risk: "low", skillId: "agenticmail-coordination", sideEffects: [] },
|
|
111
|
+
// Account management
|
|
112
|
+
{ id: "agenticmail_create_account", name: "Create Account", description: "Create agent email account", category: "write", risk: "high", skillId: "agenticmail-admin", sideEffects: [] },
|
|
113
|
+
{ id: "agenticmail_delete_agent", name: "Delete Agent", description: "Delete agent account", category: "destroy", risk: "critical", skillId: "agenticmail-admin", sideEffects: ["deletes-data"] },
|
|
114
|
+
{ id: "agenticmail_cleanup", name: "Cleanup Agents", description: "Remove inactive agents", category: "destroy", risk: "high", skillId: "agenticmail-admin", sideEffects: ["deletes-data"] },
|
|
115
|
+
{ id: "agenticmail_deletion_reports", name: "Deletion Reports", description: "List deletion reports", category: "read", risk: "low", skillId: "agenticmail-admin", sideEffects: [] },
|
|
116
|
+
{ id: "agenticmail_whoami", name: "Who Am I", description: "Get agent account info", category: "read", risk: "low", skillId: "agenticmail-admin", sideEffects: [] },
|
|
117
|
+
{ id: "agenticmail_update_metadata", name: "Update Metadata", description: "Update agent metadata", category: "write", risk: "low", skillId: "agenticmail-admin", sideEffects: [] },
|
|
118
|
+
{ id: "agenticmail_status", name: "Server Status", description: "Check server health", category: "read", risk: "low", skillId: "agenticmail-admin", sideEffects: [] },
|
|
119
|
+
{ id: "agenticmail_pending_emails", name: "Pending Emails", description: "Check blocked outbound emails", category: "read", risk: "low", skillId: "agenticmail-admin", sideEffects: [] },
|
|
120
|
+
// Organization
|
|
121
|
+
{ id: "agenticmail_contacts", name: "Contacts", description: "Manage contacts", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
122
|
+
{ id: "agenticmail_tags", name: "Tags", description: "Manage email tags/labels", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
123
|
+
{ id: "agenticmail_signatures", name: "Signatures", description: "Manage email signatures", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
124
|
+
{ id: "agenticmail_templates", name: "Templates", description: "Manage email templates", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
125
|
+
{ id: "agenticmail_template_send", name: "Send Template", description: "Send email from template", category: "communicate", risk: "high", skillId: "agenticmail", sideEffects: ["sends-email"] },
|
|
126
|
+
{ id: "agenticmail_rules", name: "Email Rules", description: "Manage auto-processing rules", category: "write", risk: "medium", skillId: "agenticmail", sideEffects: [] },
|
|
127
|
+
{ id: "agenticmail_spam", name: "Spam Management", description: "Manage spam folder", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
128
|
+
{ id: "agenticmail_drafts", name: "Drafts", description: "Manage email drafts", category: "write", risk: "low", skillId: "agenticmail", sideEffects: [] },
|
|
129
|
+
{ id: "agenticmail_schedule", name: "Schedule Email", description: "Schedule emails for later", category: "communicate", risk: "medium", skillId: "agenticmail", sideEffects: ["sends-email"] },
|
|
130
|
+
// Setup (admin only)
|
|
131
|
+
{ id: "agenticmail_setup_relay", name: "Setup Relay", description: "Configure Gmail/Outlook relay", category: "write", risk: "critical", skillId: "agenticmail-setup", sideEffects: [] },
|
|
132
|
+
{ id: "agenticmail_setup_domain", name: "Setup Domain", description: "Configure custom domain", category: "write", risk: "critical", skillId: "agenticmail-setup", sideEffects: [] },
|
|
133
|
+
{ id: "agenticmail_setup_guide", name: "Setup Guide", description: "Email setup comparison", category: "read", risk: "low", skillId: "agenticmail-setup", sideEffects: [] },
|
|
134
|
+
{ id: "agenticmail_setup_gmail_alias", name: "Gmail Alias", description: "Add Gmail send-as alias", category: "read", risk: "low", skillId: "agenticmail-setup", sideEffects: [] },
|
|
135
|
+
{ id: "agenticmail_setup_payment", name: "Setup Payment", description: "Add Cloudflare payment", category: "read", risk: "low", skillId: "agenticmail-setup", sideEffects: [] },
|
|
136
|
+
{ id: "agenticmail_purchase_domain", name: "Purchase Domain", description: "Search available domains", category: "read", risk: "low", skillId: "agenticmail-setup", sideEffects: [] },
|
|
137
|
+
{ id: "agenticmail_test_email", name: "Test Email", description: "Send test email", category: "communicate", risk: "low", skillId: "agenticmail-setup", sideEffects: ["sends-email"] },
|
|
138
|
+
{ id: "agenticmail_gateway_status", name: "Gateway Status", description: "Check email gateway", category: "read", risk: "low", skillId: "agenticmail-setup", sideEffects: [] },
|
|
139
|
+
// SMS
|
|
140
|
+
{ id: "agenticmail_sms_send", name: "Send SMS", description: "Send SMS via Google Voice", category: "communicate", risk: "high", skillId: "agenticmail-sms", sideEffects: ["sends-sms"] },
|
|
141
|
+
{ id: "agenticmail_sms_messages", name: "SMS Messages", description: "List SMS messages", category: "read", risk: "low", skillId: "agenticmail-sms", sideEffects: [] },
|
|
142
|
+
{ id: "agenticmail_sms_check_code", name: "Check SMS Code", description: "Check for verification codes", category: "read", risk: "low", skillId: "agenticmail-sms", sideEffects: [] },
|
|
143
|
+
{ id: "agenticmail_sms_read_voice", name: "Read Voice SMS", description: "Read SMS from Google Voice", category: "read", risk: "low", skillId: "agenticmail-sms", sideEffects: [] },
|
|
144
|
+
{ id: "agenticmail_sms_record", name: "Record SMS", description: "Save SMS to database", category: "write", risk: "low", skillId: "agenticmail-sms", sideEffects: [] },
|
|
145
|
+
{ id: "agenticmail_sms_parse_email", name: "Parse SMS Email", description: "Parse SMS from forwarded email", category: "read", risk: "low", skillId: "agenticmail-sms", sideEffects: [] },
|
|
146
|
+
{ id: "agenticmail_sms_setup", name: "SMS Setup", description: "Configure Google Voice SMS", category: "write", risk: "medium", skillId: "agenticmail-sms", sideEffects: [] },
|
|
147
|
+
{ id: "agenticmail_sms_config", name: "SMS Config", description: "Get SMS configuration", category: "read", risk: "low", skillId: "agenticmail-sms", sideEffects: [] }
|
|
148
|
+
];
|
|
149
|
+
ALL_TOOLS = [...OPENCLAW_CORE_TOOLS, ...AGENTICMAIL_TOOLS];
|
|
150
|
+
TOOL_INDEX = new Map(ALL_TOOLS.map((t) => [t.id, t]));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// src/engine/skills.ts
|
|
155
|
+
var PRESET_PROFILES = [
|
|
156
|
+
{
|
|
157
|
+
name: "Research Assistant",
|
|
158
|
+
description: "Can search the web, read files, and summarize content. Cannot send messages, run code, or modify anything.",
|
|
159
|
+
skills: { mode: "allowlist", list: ["research", "summarize", "data-read"] },
|
|
160
|
+
tools: { blocked: ["exec", "write", "edit"], allowed: ["web_search", "web_fetch", "read", "memory_search", "memory_get"] },
|
|
161
|
+
maxRiskLevel: "low",
|
|
162
|
+
blockedSideEffects: ["sends-email", "sends-message", "sends-sms", "posts-social", "runs-code", "modifies-files", "deletes-data", "controls-device", "financial"],
|
|
163
|
+
requireApproval: { enabled: false, forRiskLevels: [], forSideEffects: [], approvers: [], timeoutMinutes: 30 },
|
|
164
|
+
rateLimits: { toolCallsPerMinute: 30, toolCallsPerHour: 500, toolCallsPerDay: 5e3, externalActionsPerHour: 0 },
|
|
165
|
+
constraints: { maxConcurrentTasks: 3, maxSessionDurationMinutes: 480, sandboxMode: false }
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "Customer Support Agent",
|
|
169
|
+
description: "Can read/send emails, search knowledge base, and manage tickets. Cannot run code or access files.",
|
|
170
|
+
skills: { mode: "allowlist", list: ["communication", "research", "agenticmail"] },
|
|
171
|
+
tools: { blocked: ["exec", "browser", "write", "edit"], allowed: ["agenticmail_send", "agenticmail_reply", "agenticmail_inbox", "agenticmail_read", "agenticmail_search", "web_search", "web_fetch"] },
|
|
172
|
+
maxRiskLevel: "medium",
|
|
173
|
+
blockedSideEffects: ["runs-code", "modifies-files", "deletes-data", "controls-device", "financial", "posts-social"],
|
|
174
|
+
requireApproval: { enabled: true, forRiskLevels: ["high", "critical"], forSideEffects: ["sends-email"], approvers: [], timeoutMinutes: 60 },
|
|
175
|
+
rateLimits: { toolCallsPerMinute: 20, toolCallsPerHour: 300, toolCallsPerDay: 3e3, externalActionsPerHour: 50 },
|
|
176
|
+
constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: false }
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "Developer Assistant",
|
|
180
|
+
description: "Full development capabilities: code, git, GitHub, shell. Cannot send external messages or access smart home.",
|
|
181
|
+
skills: { mode: "allowlist", list: ["development", "github", "coding-agent", "research", "data"] },
|
|
182
|
+
tools: { blocked: ["agenticmail_send", "message", "tts", "nodes"], allowed: ["exec", "read", "write", "edit", "web_search", "web_fetch", "browser"] },
|
|
183
|
+
maxRiskLevel: "high",
|
|
184
|
+
blockedSideEffects: ["sends-email", "sends-message", "sends-sms", "posts-social", "controls-device", "financial"],
|
|
185
|
+
requireApproval: { enabled: true, forRiskLevels: ["critical"], forSideEffects: [], approvers: [], timeoutMinutes: 15 },
|
|
186
|
+
rateLimits: { toolCallsPerMinute: 60, toolCallsPerHour: 1e3, toolCallsPerDay: 1e4, externalActionsPerHour: 100 },
|
|
187
|
+
constraints: { maxConcurrentTasks: 3, maxSessionDurationMinutes: 720, sandboxMode: false }
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "Full Access (Owner)",
|
|
191
|
+
description: "Unrestricted access to all skills and tools. Use with caution.",
|
|
192
|
+
skills: { mode: "blocklist", list: [] },
|
|
193
|
+
tools: { blocked: [], allowed: [] },
|
|
194
|
+
maxRiskLevel: "critical",
|
|
195
|
+
blockedSideEffects: [],
|
|
196
|
+
requireApproval: { enabled: false, forRiskLevels: [], forSideEffects: [], approvers: [], timeoutMinutes: 30 },
|
|
197
|
+
rateLimits: { toolCallsPerMinute: 120, toolCallsPerHour: 5e3, toolCallsPerDay: 5e4, externalActionsPerHour: 500 },
|
|
198
|
+
constraints: { maxConcurrentTasks: 10, maxSessionDurationMinutes: 1440, sandboxMode: false }
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "Sandbox (Testing)",
|
|
202
|
+
description: "All tools available but in simulation mode. No real external actions are taken.",
|
|
203
|
+
skills: { mode: "blocklist", list: [] },
|
|
204
|
+
tools: { blocked: [], allowed: [] },
|
|
205
|
+
maxRiskLevel: "critical",
|
|
206
|
+
blockedSideEffects: [],
|
|
207
|
+
requireApproval: { enabled: false, forRiskLevels: [], forSideEffects: [], approvers: [], timeoutMinutes: 30 },
|
|
208
|
+
rateLimits: { toolCallsPerMinute: 60, toolCallsPerHour: 1e3, toolCallsPerDay: 1e4, externalActionsPerHour: 500 },
|
|
209
|
+
constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: true }
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
var BUILTIN_SKILLS = [
|
|
213
|
+
// Communication
|
|
214
|
+
{ id: "agenticmail", name: "AgenticMail", description: "Full email system \u2014 send, receive, organize, search, forward, reply. Agent-to-agent messaging and task delegation.", category: "communication", risk: "medium", icon: "\u{1F4E7}", source: "builtin" },
|
|
215
|
+
{ id: "imsg", name: "iMessage", description: "Send and receive iMessages and SMS via macOS.", category: "communication", risk: "high", icon: "\u{1F4AC}", source: "builtin", requires: ["macos"] },
|
|
216
|
+
{ id: "wacli", name: "WhatsApp", description: "Send WhatsApp messages and search chat history.", category: "communication", risk: "high", icon: "\u{1F4F1}", source: "builtin" },
|
|
217
|
+
// Development
|
|
218
|
+
{ id: "github", name: "GitHub", description: "Manage issues, PRs, CI runs, and repositories via gh CLI.", category: "development", risk: "medium", icon: "\u{1F419}", source: "builtin" },
|
|
219
|
+
{ id: "coding-agent", name: "Coding Agent", description: "Run Codex CLI, Claude Code, or other coding agents as background processes.", category: "development", risk: "high", icon: "\u{1F4BB}", source: "builtin" },
|
|
220
|
+
// Productivity
|
|
221
|
+
{ id: "gog", name: "Google Workspace", description: "Gmail, Calendar, Drive, Contacts, Sheets, and Docs.", category: "productivity", risk: "medium", icon: "\u{1F4C5}", source: "builtin" },
|
|
222
|
+
{ id: "apple-notes", name: "Apple Notes", description: "Create, search, edit, and manage Apple Notes.", category: "productivity", risk: "low", icon: "\u{1F4DD}", source: "builtin", requires: ["macos"] },
|
|
223
|
+
{ id: "apple-reminders", name: "Apple Reminders", description: "Manage Apple Reminders lists and items.", category: "productivity", risk: "low", icon: "\u2705", source: "builtin", requires: ["macos"] },
|
|
224
|
+
{ id: "bear-notes", name: "Bear Notes", description: "Create, search, and manage Bear notes.", category: "productivity", risk: "low", icon: "\u{1F43B}", source: "builtin", requires: ["macos"] },
|
|
225
|
+
{ id: "obsidian", name: "Obsidian", description: "Work with Obsidian vaults and automate via CLI.", category: "productivity", risk: "low", icon: "\u{1F48E}", source: "builtin" },
|
|
226
|
+
{ id: "things-mac", name: "Things 3", description: "Manage tasks and projects in Things 3.", category: "productivity", risk: "low", icon: "\u2611\uFE0F", source: "builtin", requires: ["macos"] },
|
|
227
|
+
// Research
|
|
228
|
+
{ id: "web-search", name: "Web Search", description: "Search the web via Brave Search API.", category: "research", risk: "low", icon: "\u{1F50D}", source: "builtin" },
|
|
229
|
+
{ id: "web-fetch", name: "Web Fetch", description: "Fetch and extract readable content from URLs.", category: "research", risk: "low", icon: "\u{1F310}", source: "builtin" },
|
|
230
|
+
{ id: "summarize", name: "Summarize", description: "Summarize or transcribe URLs, podcasts, and files.", category: "research", risk: "low", icon: "\u{1F4C4}", source: "builtin" },
|
|
231
|
+
{ id: "blogwatcher", name: "Blog Watcher", description: "Monitor blogs and RSS/Atom feeds for updates.", category: "research", risk: "low", icon: "\u{1F4E1}", source: "builtin" },
|
|
232
|
+
// Media
|
|
233
|
+
{ id: "openai-image-gen", name: "Image Generation", description: "Generate images via OpenAI Images API.", category: "media", risk: "low", icon: "\u{1F3A8}", source: "builtin" },
|
|
234
|
+
{ id: "nano-banana-pro", name: "Gemini Image", description: "Generate or edit images via Gemini 3 Pro.", category: "media", risk: "low", icon: "\u{1F5BC}\uFE0F", source: "builtin" },
|
|
235
|
+
{ id: "tts", name: "Text-to-Speech", description: "Convert text to speech audio.", category: "media", risk: "low", icon: "\u{1F50A}", source: "builtin" },
|
|
236
|
+
{ id: "openai-whisper", name: "Whisper Transcription", description: "Transcribe audio via OpenAI Whisper API.", category: "media", risk: "low", icon: "\u{1F399}\uFE0F", source: "builtin" },
|
|
237
|
+
{ id: "video-frames", name: "Video Frames", description: "Extract frames or clips from videos.", category: "media", risk: "low", icon: "\u{1F3AC}", source: "builtin" },
|
|
238
|
+
{ id: "gifgrep", name: "GIF Search", description: "Search and download GIFs.", category: "media", risk: "low", icon: "\u{1F3AD}", source: "builtin" },
|
|
239
|
+
// Automation
|
|
240
|
+
{ id: "browser", name: "Browser Control", description: "Automate web browsers \u2014 navigate, click, type, screenshot.", category: "automation", risk: "high", icon: "\u{1F30D}", source: "builtin" },
|
|
241
|
+
{ id: "exec", name: "Shell Commands", description: "Execute shell commands on the host machine.", category: "automation", risk: "critical", icon: "\u26A1", source: "builtin" },
|
|
242
|
+
{ id: "peekaboo", name: "macOS UI Automation", description: "Capture and automate macOS UI with Peekaboo.", category: "automation", risk: "high", icon: "\u{1F441}\uFE0F", source: "builtin", requires: ["macos"] },
|
|
243
|
+
{ id: "cron", name: "Scheduled Tasks", description: "Create and manage cron jobs and reminders.", category: "automation", risk: "medium", icon: "\u23F0", source: "builtin" },
|
|
244
|
+
// Smart Home
|
|
245
|
+
{ id: "openhue", name: "Philips Hue", description: "Control Hue lights and scenes.", category: "smart-home", risk: "low", icon: "\u{1F4A1}", source: "builtin" },
|
|
246
|
+
{ id: "sonoscli", name: "Sonos", description: "Control Sonos speakers.", category: "smart-home", risk: "low", icon: "\u{1F508}", source: "builtin" },
|
|
247
|
+
{ id: "blucli", name: "BluOS", description: "Control BluOS speakers.", category: "smart-home", risk: "low", icon: "\u{1F3B5}", source: "builtin" },
|
|
248
|
+
{ id: "eightctl", name: "Eight Sleep", description: "Control Eight Sleep pod temperature and alarms.", category: "smart-home", risk: "low", icon: "\u{1F6CF}\uFE0F", source: "builtin" },
|
|
249
|
+
{ id: "camsnap", name: "IP Cameras", description: "Capture frames from RTSP/ONVIF cameras.", category: "smart-home", risk: "medium", icon: "\u{1F4F7}", source: "builtin" },
|
|
250
|
+
// Data
|
|
251
|
+
{ id: "files", name: "File System", description: "Read, write, and edit files on the host.", category: "data", risk: "medium", icon: "\u{1F4C1}", source: "builtin" },
|
|
252
|
+
{ id: "memory", name: "Agent Memory", description: "Persistent memory search and storage.", category: "data", risk: "low", icon: "\u{1F9E0}", source: "builtin" },
|
|
253
|
+
// Security
|
|
254
|
+
{ id: "1password", name: "1Password", description: "Read and manage secrets via 1Password CLI.", category: "security", risk: "critical", icon: "\u{1F510}", source: "builtin" },
|
|
255
|
+
{ id: "healthcheck", name: "Security Audit", description: "Host security hardening and risk checks.", category: "security", risk: "medium", icon: "\u{1F6E1}\uFE0F", source: "builtin" },
|
|
256
|
+
// Social
|
|
257
|
+
{ id: "twitter", name: "Twitter/X", description: "Post tweets, read timeline, manage social presence.", category: "social", risk: "high", icon: "\u{1F426}", source: "builtin" },
|
|
258
|
+
// Platform
|
|
259
|
+
{ id: "gateway", name: "OpenClaw Gateway", description: "Restart, configure, and update the OpenClaw gateway.", category: "platform", risk: "critical", icon: "\u2699\uFE0F", source: "builtin" },
|
|
260
|
+
{ id: "sessions", name: "Session Management", description: "Spawn sub-agents, list sessions, send messages between sessions.", category: "platform", risk: "medium", icon: "\u{1F504}", source: "builtin" },
|
|
261
|
+
{ id: "nodes", name: "Node Control", description: "Discover and control paired devices (camera, screen, location).", category: "platform", risk: "high", icon: "\u{1F4E1}", source: "builtin" }
|
|
262
|
+
];
|
|
263
|
+
var PermissionEngine = class {
|
|
264
|
+
skills = /* @__PURE__ */ new Map();
|
|
265
|
+
profiles = /* @__PURE__ */ new Map();
|
|
266
|
+
constructor(skills) {
|
|
267
|
+
if (skills) {
|
|
268
|
+
for (const s of skills) this.skills.set(s.id, s);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
registerSkill(skill) {
|
|
272
|
+
this.skills.set(skill.id, skill);
|
|
273
|
+
}
|
|
274
|
+
setProfile(agentId, profile) {
|
|
275
|
+
this.profiles.set(agentId, profile);
|
|
276
|
+
}
|
|
277
|
+
getProfile(agentId) {
|
|
278
|
+
return this.profiles.get(agentId);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Core permission check: Can this agent use this tool right now?
|
|
282
|
+
* Returns { allowed, reason, requiresApproval }
|
|
283
|
+
*/
|
|
284
|
+
checkPermission(agentId, toolId, context) {
|
|
285
|
+
const profile = this.profiles.get(agentId);
|
|
286
|
+
if (!profile) {
|
|
287
|
+
return { allowed: false, reason: "No permission profile assigned", requiresApproval: false };
|
|
288
|
+
}
|
|
289
|
+
if (profile.constraints.sandboxMode) {
|
|
290
|
+
return { allowed: true, reason: "Sandbox mode \u2014 action will be simulated", requiresApproval: false, sandbox: true };
|
|
291
|
+
}
|
|
292
|
+
if (profile.constraints.allowedWorkingHours) {
|
|
293
|
+
const now = context?.timestamp || /* @__PURE__ */ new Date();
|
|
294
|
+
const { start, end, timezone } = profile.constraints.allowedWorkingHours;
|
|
295
|
+
const hour = parseInt(new Intl.DateTimeFormat("en-US", { hour: "numeric", hour12: false, timeZone: timezone }).format(now));
|
|
296
|
+
const startHour = parseInt(start.split(":")[0]);
|
|
297
|
+
const endHour = parseInt(end.split(":")[0]);
|
|
298
|
+
if (hour < startHour || hour >= endHour) {
|
|
299
|
+
return { allowed: false, reason: `Outside working hours (${start}-${end} ${timezone})`, requiresApproval: false };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (profile.constraints.allowedIPs?.length && context?.ip) {
|
|
303
|
+
if (!profile.constraints.allowedIPs.includes(context.ip)) {
|
|
304
|
+
return { allowed: false, reason: `IP ${context.ip} not in allowlist`, requiresApproval: false };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (profile.tools.blocked.includes(toolId)) {
|
|
308
|
+
return { allowed: false, reason: `Tool "${toolId}" is explicitly blocked`, requiresApproval: false };
|
|
309
|
+
}
|
|
310
|
+
if (profile.tools.allowed.includes(toolId)) {
|
|
311
|
+
return this._checkApproval(profile, toolId);
|
|
312
|
+
}
|
|
313
|
+
const tool = this._findTool(toolId);
|
|
314
|
+
if (!tool) {
|
|
315
|
+
return { allowed: false, reason: `Unknown tool "${toolId}"`, requiresApproval: false };
|
|
316
|
+
}
|
|
317
|
+
const skillAllowed = profile.skills.mode === "allowlist" ? profile.skills.list.includes(tool.skillId) : !profile.skills.list.includes(tool.skillId);
|
|
318
|
+
if (!skillAllowed) {
|
|
319
|
+
return { allowed: false, reason: `Skill "${tool.skillId}" is not permitted`, requiresApproval: false };
|
|
320
|
+
}
|
|
321
|
+
const riskOrder = ["low", "medium", "high", "critical"];
|
|
322
|
+
const toolRiskIdx = riskOrder.indexOf(tool.risk);
|
|
323
|
+
const maxRiskIdx = riskOrder.indexOf(profile.maxRiskLevel);
|
|
324
|
+
if (toolRiskIdx > maxRiskIdx) {
|
|
325
|
+
return { allowed: false, reason: `Tool risk "${tool.risk}" exceeds max allowed "${profile.maxRiskLevel}"`, requiresApproval: false };
|
|
326
|
+
}
|
|
327
|
+
for (const effect of tool.sideEffects) {
|
|
328
|
+
if (profile.blockedSideEffects.includes(effect)) {
|
|
329
|
+
return { allowed: false, reason: `Side effect "${effect}" is blocked`, requiresApproval: false };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return this._checkApproval(profile, toolId, tool);
|
|
333
|
+
}
|
|
334
|
+
_checkApproval(profile, toolId, tool) {
|
|
335
|
+
if (!profile.requireApproval.enabled) {
|
|
336
|
+
return { allowed: true, reason: "Permitted", requiresApproval: false };
|
|
337
|
+
}
|
|
338
|
+
if (tool) {
|
|
339
|
+
if (profile.requireApproval.forRiskLevels.includes(tool.risk)) {
|
|
340
|
+
return { allowed: true, reason: "Requires human approval (risk level)", requiresApproval: true };
|
|
341
|
+
}
|
|
342
|
+
for (const effect of tool.sideEffects) {
|
|
343
|
+
if (profile.requireApproval.forSideEffects.includes(effect)) {
|
|
344
|
+
return { allowed: true, reason: `Requires human approval (${effect})`, requiresApproval: true };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { allowed: true, reason: "Permitted", requiresApproval: false };
|
|
349
|
+
}
|
|
350
|
+
_findTool(toolId) {
|
|
351
|
+
for (const skill of this.skills.values()) {
|
|
352
|
+
const tool = skill.tools.find((t) => t.id === toolId);
|
|
353
|
+
if (tool) return tool;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const { TOOL_INDEX: TOOL_INDEX2 } = (init_tool_catalog(), __toCommonJS(tool_catalog_exports));
|
|
357
|
+
return TOOL_INDEX2.get(toolId);
|
|
358
|
+
} catch {
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get the full resolved tool list for an agent — what they can actually use
|
|
364
|
+
*/
|
|
365
|
+
getAvailableTools(agentId) {
|
|
366
|
+
const result = [];
|
|
367
|
+
for (const skill of this.skills.values()) {
|
|
368
|
+
for (const tool of skill.tools) {
|
|
369
|
+
const perm = this.checkPermission(agentId, tool.id);
|
|
370
|
+
if (perm.allowed) {
|
|
371
|
+
result.push({
|
|
372
|
+
tool,
|
|
373
|
+
status: perm.sandbox ? "sandbox" : perm.requiresApproval ? "approval-required" : "allowed"
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Generate the OpenClaw tool policy config for an agent based on their profile
|
|
382
|
+
*/
|
|
383
|
+
generateToolPolicy(agentId) {
|
|
384
|
+
const profile = this.profiles.get(agentId);
|
|
385
|
+
if (!profile) return { allowedTools: [], blockedTools: [], approvalRequired: [], rateLimits: { toolCallsPerMinute: 10, toolCallsPerHour: 100, toolCallsPerDay: 1e3, externalActionsPerHour: 10 } };
|
|
386
|
+
const allowed = [];
|
|
387
|
+
const blocked = [];
|
|
388
|
+
const approval = [];
|
|
389
|
+
for (const skill of this.skills.values()) {
|
|
390
|
+
for (const tool of skill.tools) {
|
|
391
|
+
const perm = this.checkPermission(agentId, tool.id);
|
|
392
|
+
if (perm.allowed) {
|
|
393
|
+
allowed.push(tool.id);
|
|
394
|
+
if (perm.requiresApproval) approval.push(tool.id);
|
|
395
|
+
} else {
|
|
396
|
+
blocked.push(tool.id);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { allowedTools: allowed, blockedTools: blocked, approvalRequired: approval, rateLimits: profile.rateLimits };
|
|
401
|
+
}
|
|
402
|
+
getAllSkills() {
|
|
403
|
+
return Array.from(this.skills.values());
|
|
404
|
+
}
|
|
405
|
+
getSkillsByCategory() {
|
|
406
|
+
const result = {};
|
|
407
|
+
for (const skill of this.skills.values()) {
|
|
408
|
+
if (!result[skill.category]) result[skill.category] = [];
|
|
409
|
+
result[skill.category].push(skill);
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/engine/agent-config.ts
|
|
416
|
+
var AgentConfigGenerator = class {
|
|
417
|
+
/**
|
|
418
|
+
* Generate the complete workspace files for an agent
|
|
419
|
+
*/
|
|
420
|
+
generateWorkspace(config) {
|
|
421
|
+
return {
|
|
422
|
+
"SOUL.md": this.generateSoul(config),
|
|
423
|
+
"USER.md": this.generateUser(config),
|
|
424
|
+
"AGENTS.md": this.generateAgents(config),
|
|
425
|
+
"IDENTITY.md": this.generateIdentity(config),
|
|
426
|
+
"HEARTBEAT.md": this.generateHeartbeat(config),
|
|
427
|
+
"TOOLS.md": this.generateTools(config),
|
|
428
|
+
"MEMORY.md": `# MEMORY.md \u2014 ${config.displayName}'s Long-Term Memory
|
|
429
|
+
|
|
430
|
+
_Created ${(/* @__PURE__ */ new Date()).toISOString()}_
|
|
431
|
+
`
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Generate the OpenClaw gateway config for this agent
|
|
436
|
+
*/
|
|
437
|
+
generateGatewayConfig(config) {
|
|
438
|
+
const channels = {};
|
|
439
|
+
for (const ch of config.channels.enabled) {
|
|
440
|
+
if (!ch.enabled) continue;
|
|
441
|
+
channels[ch.type] = ch.config;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
model: `${config.model.provider}/${config.model.modelId}`,
|
|
445
|
+
thinking: config.model.thinkingLevel,
|
|
446
|
+
temperature: config.model.temperature,
|
|
447
|
+
maxTokens: config.model.maxTokens,
|
|
448
|
+
channels,
|
|
449
|
+
heartbeat: config.heartbeat.enabled ? {
|
|
450
|
+
intervalMinutes: config.heartbeat.intervalMinutes
|
|
451
|
+
} : void 0,
|
|
452
|
+
workspace: config.workspace.workingDirectory
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Generate a docker-compose.yml for this agent
|
|
457
|
+
*/
|
|
458
|
+
generateDockerCompose(config) {
|
|
459
|
+
const dc = config.deployment.config.docker;
|
|
460
|
+
if (!dc) throw new Error("No Docker config");
|
|
461
|
+
const env = { ...dc.env };
|
|
462
|
+
env["OPENCLAW_MODEL"] = `${config.model.provider}/${config.model.modelId}`;
|
|
463
|
+
env["OPENCLAW_THINKING"] = config.model.thinkingLevel;
|
|
464
|
+
if (config.email.enabled && config.email.address) {
|
|
465
|
+
env["AGENTICMAIL_EMAIL"] = config.email.address;
|
|
466
|
+
}
|
|
467
|
+
const envLines = Object.entries(env).map(([k, v]) => ` ${k}: "${v}"`).join("\n");
|
|
468
|
+
const volumes = dc.volumes.map((v) => ` - ${v}`).join("\n");
|
|
469
|
+
const ports = dc.ports.map((p) => ` - "${p}:${p}"`).join("\n");
|
|
470
|
+
return `version: "3.8"
|
|
471
|
+
|
|
472
|
+
services:
|
|
473
|
+
${config.name}:
|
|
474
|
+
image: ${dc.image}:${dc.tag}
|
|
475
|
+
container_name: agenticmail-${config.name}
|
|
476
|
+
restart: ${dc.restart}
|
|
477
|
+
ports:
|
|
478
|
+
${ports}
|
|
479
|
+
volumes:
|
|
480
|
+
${volumes}
|
|
481
|
+
environment:
|
|
482
|
+
${envLines}
|
|
483
|
+
deploy:
|
|
484
|
+
resources:
|
|
485
|
+
limits:
|
|
486
|
+
cpus: "${dc.resources.cpuLimit}"
|
|
487
|
+
memory: ${dc.resources.memoryLimit}
|
|
488
|
+
${dc.network ? ` networks:
|
|
489
|
+
- ${dc.network}
|
|
490
|
+
|
|
491
|
+
networks:
|
|
492
|
+
${dc.network}:
|
|
493
|
+
external: true` : ""}
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Generate a systemd service file for VPS deployment
|
|
498
|
+
*/
|
|
499
|
+
generateSystemdUnit(config) {
|
|
500
|
+
const vps = config.deployment.config.vps;
|
|
501
|
+
if (!vps) throw new Error("No VPS config");
|
|
502
|
+
return `[Unit]
|
|
503
|
+
Description=AgenticMail Agent: ${config.displayName}
|
|
504
|
+
After=network.target
|
|
505
|
+
|
|
506
|
+
[Service]
|
|
507
|
+
Type=simple
|
|
508
|
+
User=${vps.user}
|
|
509
|
+
WorkingDirectory=${vps.installPath}
|
|
510
|
+
ExecStart=/usr/bin/env node ${vps.installPath}/node_modules/.bin/openclaw gateway start
|
|
511
|
+
Restart=always
|
|
512
|
+
RestartSec=10
|
|
513
|
+
Environment=NODE_ENV=production
|
|
514
|
+
Environment=OPENCLAW_MODEL=${config.model.provider}/${config.model.modelId}
|
|
515
|
+
Environment=OPENCLAW_THINKING=${config.model.thinkingLevel}
|
|
516
|
+
|
|
517
|
+
# Security hardening
|
|
518
|
+
NoNewPrivileges=true
|
|
519
|
+
ProtectSystem=strict
|
|
520
|
+
ProtectHome=read-only
|
|
521
|
+
ReadWritePaths=${vps.installPath}
|
|
522
|
+
PrivateTmp=true
|
|
523
|
+
|
|
524
|
+
[Install]
|
|
525
|
+
WantedBy=multi-user.target
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Generate a deployment script for VPS
|
|
530
|
+
*/
|
|
531
|
+
generateVPSDeployScript(config) {
|
|
532
|
+
const vps = config.deployment.config.vps;
|
|
533
|
+
if (!vps) throw new Error("No VPS config");
|
|
534
|
+
return `#!/bin/bash
|
|
535
|
+
set -euo pipefail
|
|
536
|
+
|
|
537
|
+
# AgenticMail Agent Deployment Script
|
|
538
|
+
# Agent: ${config.displayName}
|
|
539
|
+
# Target: ${vps.host}
|
|
540
|
+
|
|
541
|
+
echo "\u{1F680} Deploying ${config.displayName} to ${vps.host}..."
|
|
542
|
+
|
|
543
|
+
INSTALL_PATH="${vps.installPath}"
|
|
544
|
+
${vps.sudo ? 'SUDO="sudo"' : 'SUDO=""'}
|
|
545
|
+
|
|
546
|
+
# 1. Install Node.js if needed
|
|
547
|
+
if ! command -v node &> /dev/null; then
|
|
548
|
+
echo "\u{1F4E6} Installing Node.js..."
|
|
549
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | $SUDO bash -
|
|
550
|
+
$SUDO apt-get install -y nodejs
|
|
551
|
+
fi
|
|
552
|
+
|
|
553
|
+
# 2. Create workspace
|
|
554
|
+
mkdir -p "$INSTALL_PATH/workspace"
|
|
555
|
+
cd "$INSTALL_PATH"
|
|
556
|
+
|
|
557
|
+
# 3. Install OpenClaw + AgenticMail
|
|
558
|
+
echo "\u{1F4E6} Installing packages..."
|
|
559
|
+
npm init -y 2>/dev/null || true
|
|
560
|
+
npm install openclaw agenticmail @agenticmail/core @agenticmail/openclaw
|
|
561
|
+
|
|
562
|
+
# 4. Write workspace files
|
|
563
|
+
echo "\u{1F4DD} Writing agent configuration..."
|
|
564
|
+
${Object.entries(this.generateWorkspace(config)).map(
|
|
565
|
+
([file, content]) => `cat > "$INSTALL_PATH/workspace/${file}" << 'WORKSPACE_EOF'
|
|
566
|
+
${content}
|
|
567
|
+
WORKSPACE_EOF`
|
|
568
|
+
).join("\n\n")}
|
|
569
|
+
|
|
570
|
+
# 5. Write gateway config
|
|
571
|
+
cat > "$INSTALL_PATH/config.yaml" << 'CONFIG_EOF'
|
|
572
|
+
${JSON.stringify(this.generateGatewayConfig(config), null, 2)}
|
|
573
|
+
CONFIG_EOF
|
|
574
|
+
|
|
575
|
+
# 6. Install systemd service
|
|
576
|
+
echo "\u2699\uFE0F Installing systemd service..."
|
|
577
|
+
$SUDO tee /etc/systemd/system/agenticmail-${config.name}.service > /dev/null << 'SERVICE_EOF'
|
|
578
|
+
${this.generateSystemdUnit(config)}
|
|
579
|
+
SERVICE_EOF
|
|
580
|
+
|
|
581
|
+
$SUDO systemctl daemon-reload
|
|
582
|
+
$SUDO systemctl enable agenticmail-${config.name}
|
|
583
|
+
$SUDO systemctl start agenticmail-${config.name}
|
|
584
|
+
|
|
585
|
+
echo "\u2705 ${config.displayName} deployed and running!"
|
|
586
|
+
echo " Status: systemctl status agenticmail-${config.name}"
|
|
587
|
+
echo " Logs: journalctl -u agenticmail-${config.name} -f"
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
// ─── Private Generators ─────────────────────────────
|
|
591
|
+
generateSoul(config) {
|
|
592
|
+
if (config.identity.personality) return config.identity.personality;
|
|
593
|
+
const toneMap = {
|
|
594
|
+
formal: "Be professional and precise. Use proper grammar. Avoid slang or casual language.",
|
|
595
|
+
casual: "Be relaxed and conversational. Use contractions. Feel free to be informal.",
|
|
596
|
+
professional: "Be competent and clear. Direct communication without being stiff.",
|
|
597
|
+
friendly: "Be warm and approachable. Show genuine interest. Use a positive tone.",
|
|
598
|
+
custom: config.identity.customTone || ""
|
|
599
|
+
};
|
|
600
|
+
return `# SOUL.md \u2014 Who You Are
|
|
601
|
+
|
|
602
|
+
## Role
|
|
603
|
+
You are **${config.displayName}**, a ${config.identity.role}.
|
|
604
|
+
|
|
605
|
+
## Communication Style
|
|
606
|
+
${toneMap[config.identity.tone]}
|
|
607
|
+
|
|
608
|
+
## Language
|
|
609
|
+
Primary language: ${config.identity.language}
|
|
610
|
+
|
|
611
|
+
## Core Principles
|
|
612
|
+
- Be genuinely helpful, not performatively helpful
|
|
613
|
+
- Be resourceful \u2014 try to figure things out before asking
|
|
614
|
+
- Earn trust through competence
|
|
615
|
+
- Keep private information private
|
|
616
|
+
|
|
617
|
+
## Boundaries
|
|
618
|
+
- Never share confidential company information
|
|
619
|
+
- Ask before taking irreversible actions
|
|
620
|
+
- Stay within your assigned role and permissions
|
|
621
|
+
`;
|
|
622
|
+
}
|
|
623
|
+
generateUser(config) {
|
|
624
|
+
return config.context?.userInfo || `# USER.md \u2014 About Your Organization
|
|
625
|
+
|
|
626
|
+
_Configure this from the admin dashboard._
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
generateAgents(config) {
|
|
630
|
+
const customInstructions = config.context?.customInstructions || "";
|
|
631
|
+
return `# AGENTS.md \u2014 Your Workspace
|
|
632
|
+
|
|
633
|
+
## Every Session
|
|
634
|
+
1. Read SOUL.md \u2014 this is who you are
|
|
635
|
+
2. Read USER.md \u2014 this is who you're helping
|
|
636
|
+
3. Check memory/ for recent context
|
|
637
|
+
|
|
638
|
+
## Memory
|
|
639
|
+
- Daily notes: memory/YYYY-MM-DD.md
|
|
640
|
+
- Long-term: MEMORY.md
|
|
641
|
+
|
|
642
|
+
## Safety
|
|
643
|
+
- Don't exfiltrate private data
|
|
644
|
+
- Don't run destructive commands without asking
|
|
645
|
+
- When in doubt, ask
|
|
646
|
+
|
|
647
|
+
${customInstructions}
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
generateIdentity(config) {
|
|
651
|
+
return `# IDENTITY.md
|
|
652
|
+
|
|
653
|
+
- **Name:** ${config.displayName}
|
|
654
|
+
- **Role:** ${config.identity.role}
|
|
655
|
+
- **Tone:** ${config.identity.tone}
|
|
656
|
+
`;
|
|
657
|
+
}
|
|
658
|
+
generateHeartbeat(config) {
|
|
659
|
+
if (!config.heartbeat.enabled) return "# HEARTBEAT.md\n# Heartbeat disabled\n";
|
|
660
|
+
const checks = config.heartbeat.checks.map((c) => `- Check ${c}`).join("\n");
|
|
661
|
+
return `# HEARTBEAT.md
|
|
662
|
+
|
|
663
|
+
## Periodic Checks
|
|
664
|
+
${checks}
|
|
665
|
+
|
|
666
|
+
## Schedule
|
|
667
|
+
Check every ${config.heartbeat.intervalMinutes} minutes during active hours.
|
|
668
|
+
`;
|
|
669
|
+
}
|
|
670
|
+
generateTools(config) {
|
|
671
|
+
return `# TOOLS.md \u2014 Local Notes
|
|
672
|
+
|
|
673
|
+
_Add environment-specific notes here (camera names, SSH hosts, etc.)_
|
|
674
|
+
`;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/engine/deployer.ts
|
|
679
|
+
var DeploymentEngine = class {
|
|
680
|
+
configGen = new AgentConfigGenerator();
|
|
681
|
+
deployments = /* @__PURE__ */ new Map();
|
|
682
|
+
liveStatus = /* @__PURE__ */ new Map();
|
|
683
|
+
/**
|
|
684
|
+
* Deploy an agent to its configured target
|
|
685
|
+
*/
|
|
686
|
+
async deploy(config, onEvent) {
|
|
687
|
+
const events = [];
|
|
688
|
+
const emit = (phase, status, message, details) => {
|
|
689
|
+
const event = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), phase, status, message, details };
|
|
690
|
+
events.push(event);
|
|
691
|
+
onEvent?.(event);
|
|
692
|
+
};
|
|
693
|
+
try {
|
|
694
|
+
emit("validate", "started", "Validating agent configuration...");
|
|
695
|
+
this.validateConfig(config);
|
|
696
|
+
emit("validate", "completed", "Configuration valid");
|
|
697
|
+
let result;
|
|
698
|
+
switch (config.deployment.target) {
|
|
699
|
+
case "docker":
|
|
700
|
+
result = await this.deployDocker(config, emit);
|
|
701
|
+
break;
|
|
702
|
+
case "vps":
|
|
703
|
+
result = await this.deployVPS(config, emit);
|
|
704
|
+
break;
|
|
705
|
+
case "fly":
|
|
706
|
+
result = await this.deployFly(config, emit);
|
|
707
|
+
break;
|
|
708
|
+
case "railway":
|
|
709
|
+
result = await this.deployRailway(config, emit);
|
|
710
|
+
break;
|
|
711
|
+
default:
|
|
712
|
+
throw new Error(`Unsupported deployment target: ${config.deployment.target}`);
|
|
713
|
+
}
|
|
714
|
+
result.events = events;
|
|
715
|
+
this.deployments.set(config.id, result);
|
|
716
|
+
return result;
|
|
717
|
+
} catch (error) {
|
|
718
|
+
emit("complete", "failed", `Deployment failed: ${error.message}`);
|
|
719
|
+
const result = { success: false, events, error: error.message };
|
|
720
|
+
this.deployments.set(config.id, result);
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Stop a running agent
|
|
726
|
+
*/
|
|
727
|
+
async stop(config) {
|
|
728
|
+
switch (config.deployment.target) {
|
|
729
|
+
case "docker":
|
|
730
|
+
return this.execCommand(`docker stop agenticmail-${config.name} && docker rm agenticmail-${config.name}`);
|
|
731
|
+
case "vps":
|
|
732
|
+
return this.execSSH(config, `sudo systemctl stop agenticmail-${config.name}`);
|
|
733
|
+
case "fly":
|
|
734
|
+
return this.execCommand(`fly apps destroy agenticmail-${config.name} --yes`);
|
|
735
|
+
default:
|
|
736
|
+
return { success: false, message: `Cannot stop: unsupported target ${config.deployment.target}` };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Restart a running agent
|
|
741
|
+
*/
|
|
742
|
+
async restart(config) {
|
|
743
|
+
switch (config.deployment.target) {
|
|
744
|
+
case "docker":
|
|
745
|
+
return this.execCommand(`docker restart agenticmail-${config.name}`);
|
|
746
|
+
case "vps":
|
|
747
|
+
return this.execSSH(config, `sudo systemctl restart agenticmail-${config.name}`);
|
|
748
|
+
case "fly":
|
|
749
|
+
return this.execCommand(`fly apps restart agenticmail-${config.name}`);
|
|
750
|
+
default:
|
|
751
|
+
return { success: false, message: `Cannot restart: unsupported target ${config.deployment.target}` };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Get live status of a deployed agent
|
|
756
|
+
*/
|
|
757
|
+
async getStatus(config) {
|
|
758
|
+
const base = {
|
|
759
|
+
agentId: config.id,
|
|
760
|
+
name: config.displayName,
|
|
761
|
+
status: "not-deployed",
|
|
762
|
+
healthStatus: "unknown"
|
|
763
|
+
};
|
|
764
|
+
try {
|
|
765
|
+
switch (config.deployment.target) {
|
|
766
|
+
case "docker":
|
|
767
|
+
return await this.getDockerStatus(config, base);
|
|
768
|
+
case "vps":
|
|
769
|
+
return await this.getVPSStatus(config, base);
|
|
770
|
+
case "fly":
|
|
771
|
+
return await this.getCloudStatus(config, base);
|
|
772
|
+
default:
|
|
773
|
+
return base;
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
return { ...base, status: "error", healthStatus: "unhealthy" };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Stream logs from a deployed agent
|
|
781
|
+
*/
|
|
782
|
+
async getLogs(config, lines = 100) {
|
|
783
|
+
switch (config.deployment.target) {
|
|
784
|
+
case "docker":
|
|
785
|
+
return (await this.execCommand(`docker logs --tail ${lines} agenticmail-${config.name}`)).message;
|
|
786
|
+
case "vps":
|
|
787
|
+
return (await this.execSSH(config, `journalctl -u agenticmail-${config.name} --no-pager -n ${lines}`)).message;
|
|
788
|
+
case "fly":
|
|
789
|
+
return (await this.execCommand(`fly logs -a agenticmail-${config.name} -n ${lines}`)).message;
|
|
790
|
+
default:
|
|
791
|
+
return "Log streaming not supported for this target";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Update a deployed agent's configuration without full redeployment
|
|
796
|
+
*/
|
|
797
|
+
async updateConfig(config) {
|
|
798
|
+
const workspace = this.configGen.generateWorkspace(config);
|
|
799
|
+
const gatewayConfig = this.configGen.generateGatewayConfig(config);
|
|
800
|
+
switch (config.deployment.target) {
|
|
801
|
+
case "docker": {
|
|
802
|
+
for (const [file, content] of Object.entries(workspace)) {
|
|
803
|
+
const escaped = content.replace(/'/g, "'\\''");
|
|
804
|
+
await this.execCommand(`docker exec agenticmail-${config.name} sh -c 'echo "${Buffer.from(content).toString("base64")}" | base64 -d > /workspace/${file}'`);
|
|
805
|
+
}
|
|
806
|
+
await this.execCommand(`docker exec agenticmail-${config.name} openclaw gateway restart`);
|
|
807
|
+
return { success: true, message: "Configuration updated and gateway restarted" };
|
|
808
|
+
}
|
|
809
|
+
case "vps": {
|
|
810
|
+
const vps = config.deployment.config.vps;
|
|
811
|
+
for (const [file, content] of Object.entries(workspace)) {
|
|
812
|
+
await this.execSSH(config, `cat > ${vps.installPath}/workspace/${file} << 'EOF'
|
|
813
|
+
${content}
|
|
814
|
+
EOF`);
|
|
815
|
+
}
|
|
816
|
+
await this.execSSH(config, `sudo systemctl restart agenticmail-${config.name}`);
|
|
817
|
+
return { success: true, message: "Configuration updated and service restarted" };
|
|
818
|
+
}
|
|
819
|
+
default:
|
|
820
|
+
return { success: false, message: "Hot config update not supported for this target" };
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// ─── Docker Deployment ────────────────────────────────
|
|
824
|
+
async deployDocker(config, emit) {
|
|
825
|
+
const dc = config.deployment.config.docker;
|
|
826
|
+
if (!dc) throw new Error("Docker config missing");
|
|
827
|
+
emit("provision", "started", "Generating Docker configuration...");
|
|
828
|
+
const compose = this.configGen.generateDockerCompose(config);
|
|
829
|
+
emit("provision", "completed", "Docker Compose generated");
|
|
830
|
+
emit("configure", "started", "Generating agent workspace...");
|
|
831
|
+
const workspace = this.configGen.generateWorkspace(config);
|
|
832
|
+
emit("configure", "completed", `Generated ${Object.keys(workspace).length} workspace files`);
|
|
833
|
+
emit("install", "started", `Pulling image ${dc.image}:${dc.tag}...`);
|
|
834
|
+
await this.execCommand(`docker pull ${dc.image}:${dc.tag}`);
|
|
835
|
+
emit("install", "completed", "Image pulled");
|
|
836
|
+
emit("start", "started", "Starting container...");
|
|
837
|
+
const envArgs = Object.entries(dc.env).map(([k, v]) => `-e ${k}="${v}"`).join(" ");
|
|
838
|
+
const volumeArgs = dc.volumes.map((v) => `-v ${v}`).join(" ");
|
|
839
|
+
const portArgs = dc.ports.map((p) => `-p ${p}:${p}`).join(" ");
|
|
840
|
+
const runCmd = `docker run -d --name agenticmail-${config.name} --restart ${dc.restart} ${portArgs} ${volumeArgs} ${envArgs} ${dc.resources ? `--cpus="${dc.resources.cpuLimit}" --memory="${dc.resources.memoryLimit}"` : ""} ${dc.image}:${dc.tag}`;
|
|
841
|
+
const runResult = await this.execCommand(runCmd);
|
|
842
|
+
if (!runResult.success) {
|
|
843
|
+
throw new Error(`Container failed to start: ${runResult.message}`);
|
|
844
|
+
}
|
|
845
|
+
const containerId = runResult.message.trim().substring(0, 12);
|
|
846
|
+
emit("start", "completed", `Container ${containerId} running`);
|
|
847
|
+
emit("upload", "started", "Writing workspace files...");
|
|
848
|
+
for (const [file, content] of Object.entries(workspace)) {
|
|
849
|
+
await this.execCommand(`docker exec agenticmail-${config.name} sh -c 'echo "${Buffer.from(content).toString("base64")}" | base64 -d > /workspace/${file}'`);
|
|
850
|
+
}
|
|
851
|
+
emit("upload", "completed", "Workspace configured");
|
|
852
|
+
emit("healthcheck", "started", "Checking agent health...");
|
|
853
|
+
let healthy = false;
|
|
854
|
+
for (let i = 0; i < 10; i++) {
|
|
855
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
856
|
+
const check = await this.execCommand(`docker exec agenticmail-${config.name} openclaw status 2>/dev/null || echo "not ready"`);
|
|
857
|
+
if (check.success && !check.message.includes("not ready")) {
|
|
858
|
+
healthy = true;
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (healthy) {
|
|
863
|
+
emit("healthcheck", "completed", "Agent is healthy");
|
|
864
|
+
emit("complete", "completed", `Agent "${config.displayName}" deployed successfully`);
|
|
865
|
+
} else {
|
|
866
|
+
emit("healthcheck", "failed", "Agent did not become healthy within 30s");
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
success: healthy,
|
|
870
|
+
containerId,
|
|
871
|
+
url: `http://localhost:${dc.ports[0]}`,
|
|
872
|
+
events: []
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
// ─── VPS Deployment ───────────────────────────────────
|
|
876
|
+
async deployVPS(config, emit) {
|
|
877
|
+
const vps = config.deployment.config.vps;
|
|
878
|
+
if (!vps) throw new Error("VPS config missing");
|
|
879
|
+
emit("provision", "started", `Connecting to ${vps.host}...`);
|
|
880
|
+
const script = this.configGen.generateVPSDeployScript(config);
|
|
881
|
+
emit("provision", "completed", "Deploy script generated");
|
|
882
|
+
emit("configure", "started", "Testing SSH connection...");
|
|
883
|
+
const sshTest = await this.execSSH(config, 'echo "ok"');
|
|
884
|
+
if (!sshTest.success) {
|
|
885
|
+
throw new Error(`SSH connection failed: ${sshTest.message}`);
|
|
886
|
+
}
|
|
887
|
+
emit("configure", "completed", "SSH connection verified");
|
|
888
|
+
emit("upload", "started", "Uploading deployment script...");
|
|
889
|
+
const scriptB64 = Buffer.from(script).toString("base64");
|
|
890
|
+
await this.execSSH(config, `echo "${scriptB64}" | base64 -d > /tmp/deploy-agenticmail.sh && chmod +x /tmp/deploy-agenticmail.sh`);
|
|
891
|
+
emit("upload", "completed", "Script uploaded");
|
|
892
|
+
emit("install", "started", "Running deployment (this may take a few minutes)...");
|
|
893
|
+
const deployResult = await this.execSSH(config, "bash /tmp/deploy-agenticmail.sh");
|
|
894
|
+
if (!deployResult.success) {
|
|
895
|
+
throw new Error(`Deployment script failed: ${deployResult.message}`);
|
|
896
|
+
}
|
|
897
|
+
emit("install", "completed", "Installation complete");
|
|
898
|
+
emit("healthcheck", "started", "Verifying service status...");
|
|
899
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
900
|
+
const statusCheck = await this.execSSH(config, `systemctl is-active agenticmail-${config.name}`);
|
|
901
|
+
const isActive = statusCheck.success && statusCheck.message.trim() === "active";
|
|
902
|
+
if (isActive) {
|
|
903
|
+
emit("healthcheck", "completed", "Service is active");
|
|
904
|
+
emit("complete", "completed", `Agent deployed to ${vps.host}`);
|
|
905
|
+
} else {
|
|
906
|
+
emit("healthcheck", "failed", "Service not active");
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
success: isActive,
|
|
910
|
+
sshCommand: `ssh ${vps.user}@${vps.host}${vps.port !== 22 ? ` -p ${vps.port}` : ""}`,
|
|
911
|
+
events: []
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
// ─── Fly.io Deployment ────────────────────────────────
|
|
915
|
+
async deployFly(config, emit) {
|
|
916
|
+
const cloud = config.deployment.config.cloud;
|
|
917
|
+
if (!cloud || cloud.provider !== "fly") throw new Error("Fly.io config missing");
|
|
918
|
+
const appName = cloud.appName || `agenticmail-${config.name}`;
|
|
919
|
+
emit("provision", "started", `Creating Fly.io app ${appName}...`);
|
|
920
|
+
await this.execCommand(`fly apps create ${appName} --org personal`, { FLY_API_TOKEN: cloud.apiToken });
|
|
921
|
+
emit("provision", "completed", `App ${appName} created`);
|
|
922
|
+
emit("configure", "started", "Generating Dockerfile...");
|
|
923
|
+
const dockerfile = this.generateDockerfile(config);
|
|
924
|
+
const workspace = this.configGen.generateWorkspace(config);
|
|
925
|
+
const buildDir = `/tmp/agenticmail-build-${config.name}`;
|
|
926
|
+
await this.execCommand(`mkdir -p ${buildDir}/workspace`);
|
|
927
|
+
await this.writeFile(`${buildDir}/Dockerfile`, dockerfile);
|
|
928
|
+
for (const [file, content] of Object.entries(workspace)) {
|
|
929
|
+
await this.writeFile(`${buildDir}/workspace/${file}`, content);
|
|
930
|
+
}
|
|
931
|
+
const flyToml = `
|
|
932
|
+
app = "${appName}"
|
|
933
|
+
primary_region = "${cloud.region || "iad"}"
|
|
934
|
+
|
|
935
|
+
[build]
|
|
936
|
+
dockerfile = "Dockerfile"
|
|
937
|
+
|
|
938
|
+
[http_service]
|
|
939
|
+
internal_port = 3000
|
|
940
|
+
force_https = true
|
|
941
|
+
auto_stop_machines = true
|
|
942
|
+
auto_start_machines = true
|
|
943
|
+
min_machines_running = 1
|
|
944
|
+
|
|
945
|
+
[[vm]]
|
|
946
|
+
size = "${cloud.size || "shared-cpu-1x"}"
|
|
947
|
+
memory = "512mb"
|
|
948
|
+
`;
|
|
949
|
+
await this.writeFile(`${buildDir}/fly.toml`, flyToml);
|
|
950
|
+
emit("configure", "completed", "Build context ready");
|
|
951
|
+
emit("install", "started", "Deploying to Fly.io (building + pushing)...");
|
|
952
|
+
const deployResult = await this.execCommand(`cd ${buildDir} && fly deploy --now`, { FLY_API_TOKEN: cloud.apiToken });
|
|
953
|
+
emit("install", deployResult.success ? "completed" : "failed", deployResult.message);
|
|
954
|
+
await this.execCommand(`rm -rf ${buildDir}`);
|
|
955
|
+
const url = cloud.customDomain || `https://${appName}.fly.dev`;
|
|
956
|
+
if (deployResult.success) {
|
|
957
|
+
emit("complete", "completed", `Agent live at ${url}`);
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
success: deployResult.success,
|
|
961
|
+
url,
|
|
962
|
+
appId: appName,
|
|
963
|
+
events: []
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
// ─── Railway Deployment ───────────────────────────────
|
|
967
|
+
async deployRailway(config, emit) {
|
|
968
|
+
const cloud = config.deployment.config.cloud;
|
|
969
|
+
if (!cloud || cloud.provider !== "railway") throw new Error("Railway config missing");
|
|
970
|
+
emit("provision", "started", "Creating Railway project...");
|
|
971
|
+
const appName = cloud.appName || `agenticmail-${config.name}`;
|
|
972
|
+
const result = await this.execCommand(`railway init --name ${appName}`, { RAILWAY_TOKEN: cloud.apiToken });
|
|
973
|
+
emit("provision", result.success ? "completed" : "failed", result.message);
|
|
974
|
+
return {
|
|
975
|
+
success: result.success,
|
|
976
|
+
url: `https://${appName}.up.railway.app`,
|
|
977
|
+
appId: appName,
|
|
978
|
+
events: []
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
// ─── Status Checkers ──────────────────────────────────
|
|
982
|
+
async getDockerStatus(config, base) {
|
|
983
|
+
const inspect = await this.execCommand(`docker inspect agenticmail-${config.name} --format '{{.State.Status}} {{.State.StartedAt}}'`);
|
|
984
|
+
if (!inspect.success) return { ...base, status: "not-deployed" };
|
|
985
|
+
const [status, startedAt] = inspect.message.trim().split(" ");
|
|
986
|
+
const running = status === "running";
|
|
987
|
+
const uptime = running ? Math.floor((Date.now() - new Date(startedAt).getTime()) / 1e3) : 0;
|
|
988
|
+
let metrics = void 0;
|
|
989
|
+
if (running) {
|
|
990
|
+
const stats = await this.execCommand(`docker stats agenticmail-${config.name} --no-stream --format '{{.CPUPerc}} {{.MemUsage}}'`);
|
|
991
|
+
if (stats.success) {
|
|
992
|
+
const parts = stats.message.trim().split(" ");
|
|
993
|
+
metrics = {
|
|
994
|
+
cpuPercent: parseFloat(parts[0]) || 0,
|
|
995
|
+
memoryMb: parseFloat(parts[1]) || 0,
|
|
996
|
+
toolCallsToday: 0,
|
|
997
|
+
activeSessionCount: 0,
|
|
998
|
+
errorRate: 0
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
...base,
|
|
1004
|
+
status: running ? "running" : "stopped",
|
|
1005
|
+
uptime,
|
|
1006
|
+
healthStatus: running ? "healthy" : "unhealthy",
|
|
1007
|
+
lastHealthCheck: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1008
|
+
metrics
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async getVPSStatus(config, base) {
|
|
1012
|
+
const result = await this.execSSH(config, `systemctl is-active agenticmail-${config.name}`);
|
|
1013
|
+
const active = result.success && result.message.trim() === "active";
|
|
1014
|
+
let uptime = 0;
|
|
1015
|
+
if (active) {
|
|
1016
|
+
const uptimeResult = await this.execSSH(config, `systemctl show agenticmail-${config.name} --property=ActiveEnterTimestamp --value`);
|
|
1017
|
+
if (uptimeResult.success) {
|
|
1018
|
+
uptime = Math.floor((Date.now() - new Date(uptimeResult.message.trim()).getTime()) / 1e3);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
...base,
|
|
1023
|
+
status: active ? "running" : "stopped",
|
|
1024
|
+
uptime,
|
|
1025
|
+
healthStatus: active ? "healthy" : "unhealthy",
|
|
1026
|
+
lastHealthCheck: (/* @__PURE__ */ new Date()).toISOString()
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
async getCloudStatus(config, base) {
|
|
1030
|
+
const cloud = config.deployment.config.cloud;
|
|
1031
|
+
if (!cloud) return base;
|
|
1032
|
+
const appName = cloud.appName || `agenticmail-${config.name}`;
|
|
1033
|
+
const result = await this.execCommand(`fly status -a ${appName} --json`, { FLY_API_TOKEN: cloud.apiToken });
|
|
1034
|
+
if (!result.success) return { ...base, status: "error" };
|
|
1035
|
+
try {
|
|
1036
|
+
const status = JSON.parse(result.message);
|
|
1037
|
+
return {
|
|
1038
|
+
...base,
|
|
1039
|
+
status: status.Deployed ? "running" : "stopped",
|
|
1040
|
+
healthStatus: status.Deployed ? "healthy" : "unhealthy",
|
|
1041
|
+
endpoint: `https://${appName}.fly.dev`,
|
|
1042
|
+
version: status.Version?.toString()
|
|
1043
|
+
};
|
|
1044
|
+
} catch {
|
|
1045
|
+
return { ...base, status: "error" };
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// ─── Helpers ──────────────────────────────────────────
|
|
1049
|
+
validateConfig(config) {
|
|
1050
|
+
if (!config.name) throw new Error("Agent name is required");
|
|
1051
|
+
if (!config.identity.role) throw new Error("Agent role is required");
|
|
1052
|
+
if (!config.model.modelId) throw new Error("Model ID is required");
|
|
1053
|
+
if (!config.deployment.target) throw new Error("Deployment target is required");
|
|
1054
|
+
switch (config.deployment.target) {
|
|
1055
|
+
case "docker":
|
|
1056
|
+
if (!config.deployment.config.docker) throw new Error("Docker configuration missing");
|
|
1057
|
+
break;
|
|
1058
|
+
case "vps":
|
|
1059
|
+
if (!config.deployment.config.vps?.host) throw new Error("VPS host is required");
|
|
1060
|
+
break;
|
|
1061
|
+
case "fly":
|
|
1062
|
+
case "railway":
|
|
1063
|
+
if (!config.deployment.config.cloud?.apiToken) throw new Error("Cloud API token is required");
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
generateDockerfile(config) {
|
|
1068
|
+
return `FROM node:22-slim
|
|
1069
|
+
|
|
1070
|
+
WORKDIR /app
|
|
1071
|
+
|
|
1072
|
+
RUN npm install -g openclaw agenticmail @agenticmail/core @agenticmail/openclaw
|
|
1073
|
+
|
|
1074
|
+
COPY workspace/ /workspace/
|
|
1075
|
+
|
|
1076
|
+
ENV NODE_ENV=production
|
|
1077
|
+
ENV OPENCLAW_MODEL=${config.model.provider}/${config.model.modelId}
|
|
1078
|
+
ENV OPENCLAW_THINKING=${config.model.thinkingLevel}
|
|
1079
|
+
|
|
1080
|
+
EXPOSE 3000
|
|
1081
|
+
|
|
1082
|
+
CMD ["openclaw", "gateway", "start"]
|
|
1083
|
+
`;
|
|
1084
|
+
}
|
|
1085
|
+
async execCommand(cmd, env) {
|
|
1086
|
+
const { exec } = await import("child_process");
|
|
1087
|
+
const { promisify } = await import("util");
|
|
1088
|
+
const execAsync = promisify(exec);
|
|
1089
|
+
try {
|
|
1090
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
1091
|
+
timeout: 3e5,
|
|
1092
|
+
// 5 min max
|
|
1093
|
+
env: { ...process.env, ...env }
|
|
1094
|
+
});
|
|
1095
|
+
return { success: true, message: stdout || stderr };
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
return { success: false, message: error.stderr || error.message };
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async execSSH(config, command) {
|
|
1101
|
+
const vps = config.deployment.config.vps;
|
|
1102
|
+
if (!vps) return { success: false, message: "No VPS config" };
|
|
1103
|
+
const sshArgs = [
|
|
1104
|
+
"-o StrictHostKeyChecking=no",
|
|
1105
|
+
`-p ${vps.port || 22}`,
|
|
1106
|
+
vps.sshKeyPath ? `-i ${vps.sshKeyPath}` : "",
|
|
1107
|
+
`${vps.user}@${vps.host}`,
|
|
1108
|
+
`"${command.replace(/"/g, '\\"')}"`
|
|
1109
|
+
].filter(Boolean).join(" ");
|
|
1110
|
+
return this.execCommand(`ssh ${sshArgs}`);
|
|
1111
|
+
}
|
|
1112
|
+
async writeFile(path, content) {
|
|
1113
|
+
const { writeFile } = await import("fs/promises");
|
|
1114
|
+
const { dirname } = await import("path");
|
|
1115
|
+
const { mkdir } = await import("fs/promises");
|
|
1116
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1117
|
+
await writeFile(path, content, "utf-8");
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
// src/engine/approvals.ts
|
|
1122
|
+
var ApprovalEngine = class {
|
|
1123
|
+
requests = /* @__PURE__ */ new Map();
|
|
1124
|
+
policies = [];
|
|
1125
|
+
listeners = [];
|
|
1126
|
+
addPolicy(policy) {
|
|
1127
|
+
this.policies.push(policy);
|
|
1128
|
+
}
|
|
1129
|
+
removePolicy(id) {
|
|
1130
|
+
this.policies = this.policies.filter((p) => p.id !== id);
|
|
1131
|
+
}
|
|
1132
|
+
getPolicies() {
|
|
1133
|
+
return [...this.policies];
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Check if a tool call needs approval and create a request if so
|
|
1137
|
+
*/
|
|
1138
|
+
async requestApproval(opts) {
|
|
1139
|
+
const policy = this.findMatchingPolicy(opts.toolId, opts.riskLevel, opts.sideEffects);
|
|
1140
|
+
if (!policy) return null;
|
|
1141
|
+
const request = {
|
|
1142
|
+
id: crypto.randomUUID(),
|
|
1143
|
+
agentId: opts.agentId,
|
|
1144
|
+
agentName: opts.agentName,
|
|
1145
|
+
toolId: opts.toolId,
|
|
1146
|
+
toolName: opts.toolName,
|
|
1147
|
+
reason: `Policy "${policy.name}" requires approval`,
|
|
1148
|
+
riskLevel: opts.riskLevel,
|
|
1149
|
+
sideEffects: opts.sideEffects,
|
|
1150
|
+
parameters: this.sanitizeParams(opts.parameters),
|
|
1151
|
+
context: opts.context,
|
|
1152
|
+
status: "pending",
|
|
1153
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1154
|
+
expiresAt: new Date(Date.now() + policy.timeout.minutes * 6e4).toISOString()
|
|
1155
|
+
};
|
|
1156
|
+
this.requests.set(request.id, request);
|
|
1157
|
+
await this.notifyApprovers(request, policy);
|
|
1158
|
+
setTimeout(() => {
|
|
1159
|
+
const req = this.requests.get(request.id);
|
|
1160
|
+
if (req && req.status === "pending") {
|
|
1161
|
+
req.status = "expired";
|
|
1162
|
+
if (policy.timeout.defaultAction === "allow") {
|
|
1163
|
+
req.status = "approved";
|
|
1164
|
+
req.decision = {
|
|
1165
|
+
by: "system",
|
|
1166
|
+
action: "approve",
|
|
1167
|
+
reason: "Auto-approved: approval timeout expired",
|
|
1168
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
this.notifyListeners(req);
|
|
1172
|
+
}
|
|
1173
|
+
}, policy.timeout.minutes * 6e4);
|
|
1174
|
+
this.notifyListeners(request);
|
|
1175
|
+
return request;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Approve or deny a pending request
|
|
1179
|
+
*/
|
|
1180
|
+
decide(requestId, decision) {
|
|
1181
|
+
const request = this.requests.get(requestId);
|
|
1182
|
+
if (!request || request.status !== "pending") return null;
|
|
1183
|
+
request.status = decision.action === "approve" ? "approved" : "denied";
|
|
1184
|
+
request.decision = { ...decision, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1185
|
+
this.notifyListeners(request);
|
|
1186
|
+
return request;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Get all pending requests (for the dashboard)
|
|
1190
|
+
*/
|
|
1191
|
+
getPendingRequests(agentId) {
|
|
1192
|
+
const all = Array.from(this.requests.values()).filter((r) => r.status === "pending");
|
|
1193
|
+
return agentId ? all.filter((r) => r.agentId === agentId) : all;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Get request by ID
|
|
1197
|
+
*/
|
|
1198
|
+
getRequest(id) {
|
|
1199
|
+
return this.requests.get(id);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Get history of all requests
|
|
1203
|
+
*/
|
|
1204
|
+
getHistory(opts) {
|
|
1205
|
+
let all = Array.from(this.requests.values()).sort(
|
|
1206
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
1207
|
+
);
|
|
1208
|
+
if (opts?.agentId) all = all.filter((r) => r.agentId === opts.agentId);
|
|
1209
|
+
const total = all.length;
|
|
1210
|
+
const offset = opts?.offset || 0;
|
|
1211
|
+
const limit = opts?.limit || 25;
|
|
1212
|
+
return { requests: all.slice(offset, offset + limit), total };
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Wait for a specific request to be decided (for sync approval flows)
|
|
1216
|
+
*/
|
|
1217
|
+
async waitForDecision(requestId, timeoutMs = 3e5) {
|
|
1218
|
+
return new Promise((resolve, reject) => {
|
|
1219
|
+
const check = setInterval(() => {
|
|
1220
|
+
const req = this.requests.get(requestId);
|
|
1221
|
+
if (req && req.status !== "pending") {
|
|
1222
|
+
clearInterval(check);
|
|
1223
|
+
clearTimeout(timeout);
|
|
1224
|
+
resolve(req);
|
|
1225
|
+
}
|
|
1226
|
+
}, 1e3);
|
|
1227
|
+
const timeout = setTimeout(() => {
|
|
1228
|
+
clearInterval(check);
|
|
1229
|
+
const req = this.requests.get(requestId);
|
|
1230
|
+
if (req) {
|
|
1231
|
+
req.status = "expired";
|
|
1232
|
+
resolve(req);
|
|
1233
|
+
} else {
|
|
1234
|
+
reject(new Error("Request not found"));
|
|
1235
|
+
}
|
|
1236
|
+
}, timeoutMs);
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Subscribe to approval request changes
|
|
1241
|
+
*/
|
|
1242
|
+
onRequest(listener) {
|
|
1243
|
+
this.listeners.push(listener);
|
|
1244
|
+
return () => {
|
|
1245
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
// ─── Private ──────────────────────────────────────────
|
|
1249
|
+
findMatchingPolicy(toolId, riskLevel, sideEffects) {
|
|
1250
|
+
return this.policies.find((p) => {
|
|
1251
|
+
if (!p.enabled) return false;
|
|
1252
|
+
if (p.triggers.toolIds?.includes(toolId)) return true;
|
|
1253
|
+
if (p.triggers.riskLevels?.includes(riskLevel)) return true;
|
|
1254
|
+
if (p.triggers.sideEffects?.some((e) => sideEffects.includes(e))) return true;
|
|
1255
|
+
if (p.triggers.allExternalActions && sideEffects.length > 0) return true;
|
|
1256
|
+
return false;
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
sanitizeParams(params) {
|
|
1260
|
+
if (!params) return void 0;
|
|
1261
|
+
const sanitized = { ...params };
|
|
1262
|
+
for (const key of ["password", "token", "secret", "key", "apiKey", "credential"]) {
|
|
1263
|
+
if (key in sanitized) sanitized[key] = "***";
|
|
1264
|
+
}
|
|
1265
|
+
return sanitized;
|
|
1266
|
+
}
|
|
1267
|
+
async notifyApprovers(request, policy) {
|
|
1268
|
+
for (const channel of policy.notify.channels) {
|
|
1269
|
+
switch (channel) {
|
|
1270
|
+
case "webhook":
|
|
1271
|
+
if (policy.notify.webhookUrl) {
|
|
1272
|
+
try {
|
|
1273
|
+
await fetch(policy.notify.webhookUrl, {
|
|
1274
|
+
method: "POST",
|
|
1275
|
+
headers: { "Content-Type": "application/json" },
|
|
1276
|
+
body: JSON.stringify({ type: "approval_request", request })
|
|
1277
|
+
});
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
notifyListeners(request) {
|
|
1286
|
+
for (const listener of this.listeners) {
|
|
1287
|
+
try {
|
|
1288
|
+
listener(request);
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
// src/engine/lifecycle.ts
|
|
1296
|
+
var AgentLifecycleManager = class {
|
|
1297
|
+
agents = /* @__PURE__ */ new Map();
|
|
1298
|
+
healthCheckIntervals = /* @__PURE__ */ new Map();
|
|
1299
|
+
deployer = new DeploymentEngine();
|
|
1300
|
+
configGen = new AgentConfigGenerator();
|
|
1301
|
+
permissions;
|
|
1302
|
+
db;
|
|
1303
|
+
eventListeners = [];
|
|
1304
|
+
constructor(opts) {
|
|
1305
|
+
this.db = opts?.db;
|
|
1306
|
+
this.permissions = opts?.permissions || new PermissionEngine();
|
|
1307
|
+
}
|
|
1308
|
+
// ─── Agent CRUD ─────────────────────────────────────
|
|
1309
|
+
/**
|
|
1310
|
+
* Create a new managed agent (starts in 'draft' state)
|
|
1311
|
+
*/
|
|
1312
|
+
async createAgent(orgId, config, createdBy) {
|
|
1313
|
+
const agent = {
|
|
1314
|
+
id: config.id || crypto.randomUUID(),
|
|
1315
|
+
orgId,
|
|
1316
|
+
config,
|
|
1317
|
+
state: "draft",
|
|
1318
|
+
stateHistory: [],
|
|
1319
|
+
health: {
|
|
1320
|
+
status: "unknown",
|
|
1321
|
+
lastCheck: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1322
|
+
uptime: 0,
|
|
1323
|
+
consecutiveFailures: 0,
|
|
1324
|
+
checks: []
|
|
1325
|
+
},
|
|
1326
|
+
usage: this.emptyUsage(),
|
|
1327
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1328
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1329
|
+
version: 1
|
|
1330
|
+
};
|
|
1331
|
+
this.agents.set(agent.id, agent);
|
|
1332
|
+
await this.persistAgent(agent);
|
|
1333
|
+
this.emitEvent(agent, "created", { createdBy });
|
|
1334
|
+
return agent;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Update agent configuration (must be in draft, ready, stopped, or error state)
|
|
1338
|
+
*/
|
|
1339
|
+
async updateConfig(agentId, updates, updatedBy) {
|
|
1340
|
+
const agent = this.getAgent(agentId);
|
|
1341
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1342
|
+
const mutableStates = ["draft", "ready", "stopped", "error"];
|
|
1343
|
+
if (!mutableStates.includes(agent.state)) {
|
|
1344
|
+
throw new Error(`Cannot update config in state "${agent.state}". Stop the agent first.`);
|
|
1345
|
+
}
|
|
1346
|
+
agent.config = { ...agent.config, ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1347
|
+
agent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1348
|
+
agent.version++;
|
|
1349
|
+
if (agent.state === "draft" && this.isConfigComplete(agent.config)) {
|
|
1350
|
+
this.transition(agent, "ready", "Configuration complete", updatedBy);
|
|
1351
|
+
} else if (agent.state !== "draft") {
|
|
1352
|
+
this.transition(agent, "ready", "Configuration updated", updatedBy);
|
|
1353
|
+
}
|
|
1354
|
+
await this.persistAgent(agent);
|
|
1355
|
+
this.emitEvent(agent, "configured", { updatedBy, changes: Object.keys(updates) });
|
|
1356
|
+
return agent;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Deploy an agent to its target environment
|
|
1360
|
+
*/
|
|
1361
|
+
async deploy(agentId, deployedBy) {
|
|
1362
|
+
const agent = this.getAgent(agentId);
|
|
1363
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1364
|
+
if (!["ready", "stopped", "error"].includes(agent.state)) {
|
|
1365
|
+
throw new Error(`Cannot deploy from state "${agent.state}"`);
|
|
1366
|
+
}
|
|
1367
|
+
if (!this.isConfigComplete(agent.config)) {
|
|
1368
|
+
throw new Error("Agent configuration is incomplete");
|
|
1369
|
+
}
|
|
1370
|
+
this.transition(agent, "provisioning", "Deployment initiated", deployedBy);
|
|
1371
|
+
await this.persistAgent(agent);
|
|
1372
|
+
try {
|
|
1373
|
+
this.transition(agent, "deploying", "Pushing configuration", "system");
|
|
1374
|
+
const result = await this.deployer.deploy(agent.config, (event) => {
|
|
1375
|
+
this.emitEvent(agent, "deployed", { phase: event.phase, status: event.status, message: event.message });
|
|
1376
|
+
});
|
|
1377
|
+
if (result.success) {
|
|
1378
|
+
this.transition(agent, "starting", "Deployment successful, agent starting", "system");
|
|
1379
|
+
agent.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1380
|
+
const healthy = await this.waitForHealthy(agent, 6e4);
|
|
1381
|
+
if (healthy) {
|
|
1382
|
+
this.transition(agent, "running", "Agent is healthy and running", "system");
|
|
1383
|
+
this.startHealthCheckLoop(agent);
|
|
1384
|
+
} else {
|
|
1385
|
+
this.transition(agent, "degraded", "Agent started but health check failed", "system");
|
|
1386
|
+
this.startHealthCheckLoop(agent);
|
|
1387
|
+
}
|
|
1388
|
+
} else {
|
|
1389
|
+
this.transition(agent, "error", `Deployment failed: ${result.error}`, "system");
|
|
1390
|
+
}
|
|
1391
|
+
await this.persistAgent(agent);
|
|
1392
|
+
return agent;
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
this.transition(agent, "error", `Deployment error: ${error.message}`, "system");
|
|
1395
|
+
await this.persistAgent(agent);
|
|
1396
|
+
throw error;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Stop a running agent
|
|
1401
|
+
*/
|
|
1402
|
+
async stop(agentId, stoppedBy, reason) {
|
|
1403
|
+
const agent = this.getAgent(agentId);
|
|
1404
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1405
|
+
if (!["running", "degraded", "starting", "error"].includes(agent.state)) {
|
|
1406
|
+
throw new Error(`Cannot stop from state "${agent.state}"`);
|
|
1407
|
+
}
|
|
1408
|
+
this.stopHealthCheckLoop(agentId);
|
|
1409
|
+
try {
|
|
1410
|
+
await this.deployer.stop(agent.config);
|
|
1411
|
+
this.transition(agent, "stopped", reason || "Stopped by user", stoppedBy);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
this.transition(agent, "stopped", `Stopped with error: ${error.message}`, stoppedBy);
|
|
1414
|
+
}
|
|
1415
|
+
await this.persistAgent(agent);
|
|
1416
|
+
this.emitEvent(agent, "stopped", { stoppedBy, reason });
|
|
1417
|
+
return agent;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Restart a running agent
|
|
1421
|
+
*/
|
|
1422
|
+
async restart(agentId, restartedBy) {
|
|
1423
|
+
const agent = this.getAgent(agentId);
|
|
1424
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1425
|
+
this.transition(agent, "updating", "Restarting", restartedBy);
|
|
1426
|
+
try {
|
|
1427
|
+
await this.deployer.restart(agent.config);
|
|
1428
|
+
const healthy = await this.waitForHealthy(agent, 3e4);
|
|
1429
|
+
this.transition(agent, healthy ? "running" : "degraded", "Restarted", "system");
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
this.transition(agent, "error", `Restart failed: ${error.message}`, "system");
|
|
1432
|
+
}
|
|
1433
|
+
await this.persistAgent(agent);
|
|
1434
|
+
this.emitEvent(agent, "restarted", { restartedBy });
|
|
1435
|
+
return agent;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Hot-update config on a running agent (no full redeploy)
|
|
1439
|
+
*/
|
|
1440
|
+
async hotUpdate(agentId, updates, updatedBy) {
|
|
1441
|
+
const agent = this.getAgent(agentId);
|
|
1442
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1443
|
+
if (agent.state !== "running" && agent.state !== "degraded") {
|
|
1444
|
+
throw new Error(`Hot update only works on running agents (current: "${agent.state}")`);
|
|
1445
|
+
}
|
|
1446
|
+
const prevState = agent.state;
|
|
1447
|
+
this.transition(agent, "updating", "Hot config update", updatedBy);
|
|
1448
|
+
agent.config = { ...agent.config, ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1449
|
+
agent.version++;
|
|
1450
|
+
try {
|
|
1451
|
+
await this.deployer.updateConfig(agent.config);
|
|
1452
|
+
this.transition(agent, prevState, "Config updated successfully", "system");
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
this.transition(agent, "degraded", `Config update failed: ${error.message}`, "system");
|
|
1455
|
+
}
|
|
1456
|
+
await this.persistAgent(agent);
|
|
1457
|
+
this.emitEvent(agent, "updated", { updatedBy, hotUpdate: true });
|
|
1458
|
+
return agent;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Destroy an agent completely (stop + delete all resources)
|
|
1462
|
+
*/
|
|
1463
|
+
async destroy(agentId, destroyedBy) {
|
|
1464
|
+
const agent = this.getAgent(agentId);
|
|
1465
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1466
|
+
this.transition(agent, "destroying", "Agent being destroyed", destroyedBy);
|
|
1467
|
+
this.stopHealthCheckLoop(agentId);
|
|
1468
|
+
if (["running", "degraded", "starting"].includes(agent.state)) {
|
|
1469
|
+
try {
|
|
1470
|
+
await this.deployer.stop(agent.config);
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
this.emitEvent(agent, "destroyed", { destroyedBy });
|
|
1475
|
+
this.agents.delete(agentId);
|
|
1476
|
+
}
|
|
1477
|
+
// ─── Monitoring ─────────────────────────────────────
|
|
1478
|
+
/**
|
|
1479
|
+
* Record a tool call for usage tracking
|
|
1480
|
+
*/
|
|
1481
|
+
recordToolCall(agentId, toolId, opts) {
|
|
1482
|
+
const agent = this.agents.get(agentId);
|
|
1483
|
+
if (!agent) return;
|
|
1484
|
+
const usage = agent.usage;
|
|
1485
|
+
usage.toolCallsToday++;
|
|
1486
|
+
usage.toolCallsThisMonth++;
|
|
1487
|
+
if (opts?.tokensUsed) {
|
|
1488
|
+
usage.tokensToday += opts.tokensUsed;
|
|
1489
|
+
usage.tokensThisMonth += opts.tokensUsed;
|
|
1490
|
+
}
|
|
1491
|
+
if (opts?.costUsd) {
|
|
1492
|
+
usage.costToday += opts.costUsd;
|
|
1493
|
+
usage.costThisMonth += opts.costUsd;
|
|
1494
|
+
}
|
|
1495
|
+
if (opts?.isExternalAction) {
|
|
1496
|
+
usage.externalActionsToday++;
|
|
1497
|
+
usage.externalActionsThisMonth++;
|
|
1498
|
+
}
|
|
1499
|
+
if (opts?.error) {
|
|
1500
|
+
usage.errorsToday++;
|
|
1501
|
+
}
|
|
1502
|
+
usage.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1503
|
+
if (usage.tokenBudgetMonthly > 0 && usage.tokensThisMonth >= usage.tokenBudgetMonthly) {
|
|
1504
|
+
this.emitEvent(agent, "budget_exceeded", { type: "tokens", used: usage.tokensThisMonth, budget: usage.tokenBudgetMonthly });
|
|
1505
|
+
this.stop(agentId, "system", "Monthly token budget exceeded").catch(() => {
|
|
1506
|
+
});
|
|
1507
|
+
} else if (usage.tokenBudgetMonthly > 0 && usage.tokensThisMonth >= usage.tokenBudgetMonthly * 0.8) {
|
|
1508
|
+
this.emitEvent(agent, "budget_warning", { type: "tokens", used: usage.tokensThisMonth, budget: usage.tokenBudgetMonthly, percent: 80 });
|
|
1509
|
+
}
|
|
1510
|
+
if (usage.costBudgetMonthly > 0 && usage.costThisMonth >= usage.costBudgetMonthly) {
|
|
1511
|
+
this.emitEvent(agent, "budget_exceeded", { type: "cost", used: usage.costThisMonth, budget: usage.costBudgetMonthly });
|
|
1512
|
+
this.stop(agentId, "system", "Monthly cost budget exceeded").catch(() => {
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
this.emitEvent(agent, "tool_call", { toolId, ...opts });
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Get all agents for an org
|
|
1519
|
+
*/
|
|
1520
|
+
getAgentsByOrg(orgId) {
|
|
1521
|
+
return Array.from(this.agents.values()).filter((a) => a.orgId === orgId);
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Get a single agent
|
|
1525
|
+
*/
|
|
1526
|
+
getAgent(agentId) {
|
|
1527
|
+
return this.agents.get(agentId);
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Get org-wide usage summary
|
|
1531
|
+
*/
|
|
1532
|
+
getOrgUsage(orgId) {
|
|
1533
|
+
const agents = this.getAgentsByOrg(orgId);
|
|
1534
|
+
return {
|
|
1535
|
+
totalAgents: agents.length,
|
|
1536
|
+
runningAgents: agents.filter((a) => a.state === "running").length,
|
|
1537
|
+
totalTokensToday: agents.reduce((sum, a) => sum + a.usage.tokensToday, 0),
|
|
1538
|
+
totalCostToday: agents.reduce((sum, a) => sum + a.usage.costToday, 0),
|
|
1539
|
+
totalToolCallsToday: agents.reduce((sum, a) => sum + a.usage.toolCallsToday, 0),
|
|
1540
|
+
totalErrorsToday: agents.reduce((sum, a) => sum + a.usage.errorsToday, 0),
|
|
1541
|
+
agents: agents.map((a) => ({ id: a.id, name: a.config.displayName, state: a.state, usage: a.usage }))
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Subscribe to lifecycle events (for dashboard real-time updates)
|
|
1546
|
+
*/
|
|
1547
|
+
onEvent(listener) {
|
|
1548
|
+
this.eventListeners.push(listener);
|
|
1549
|
+
return () => {
|
|
1550
|
+
this.eventListeners = this.eventListeners.filter((l) => l !== listener);
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Reset daily counters (call at midnight via cron)
|
|
1555
|
+
*/
|
|
1556
|
+
resetDailyCounters() {
|
|
1557
|
+
for (const agent of this.agents.values()) {
|
|
1558
|
+
agent.usage.tokensToday = 0;
|
|
1559
|
+
agent.usage.toolCallsToday = 0;
|
|
1560
|
+
agent.usage.externalActionsToday = 0;
|
|
1561
|
+
agent.usage.costToday = 0;
|
|
1562
|
+
agent.usage.errorsToday = 0;
|
|
1563
|
+
agent.usage.totalSessionsToday = 0;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Reset monthly counters (call on 1st of month)
|
|
1568
|
+
*/
|
|
1569
|
+
resetMonthlyCounters() {
|
|
1570
|
+
for (const agent of this.agents.values()) {
|
|
1571
|
+
agent.usage.tokensThisMonth = 0;
|
|
1572
|
+
agent.usage.toolCallsThisMonth = 0;
|
|
1573
|
+
agent.usage.externalActionsThisMonth = 0;
|
|
1574
|
+
agent.usage.costThisMonth = 0;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
// ─── Health Check Loop ────────────────────────────────
|
|
1578
|
+
startHealthCheckLoop(agent) {
|
|
1579
|
+
this.stopHealthCheckLoop(agent.id);
|
|
1580
|
+
const interval = setInterval(async () => {
|
|
1581
|
+
try {
|
|
1582
|
+
const status = await this.deployer.getStatus(agent.config);
|
|
1583
|
+
agent.lastHealthCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1584
|
+
const check = {
|
|
1585
|
+
name: "deployment_status",
|
|
1586
|
+
status: status.status === "running" ? "pass" : "fail",
|
|
1587
|
+
message: `Status: ${status.status}, Health: ${status.healthStatus}`,
|
|
1588
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1589
|
+
durationMs: 0
|
|
1590
|
+
};
|
|
1591
|
+
agent.health.checks = [check, ...agent.health.checks].slice(0, 10);
|
|
1592
|
+
if (status.status === "running" && status.healthStatus === "healthy") {
|
|
1593
|
+
agent.health.status = "healthy";
|
|
1594
|
+
agent.health.consecutiveFailures = 0;
|
|
1595
|
+
if (status.uptime) agent.health.uptime = status.uptime;
|
|
1596
|
+
if (status.metrics) {
|
|
1597
|
+
agent.usage.activeSessionCount = status.metrics.activeSessionCount;
|
|
1598
|
+
}
|
|
1599
|
+
if (agent.state === "degraded") {
|
|
1600
|
+
this.transition(agent, "running", "Health restored", "system");
|
|
1601
|
+
this.emitEvent(agent, "auto_recovered", {});
|
|
1602
|
+
}
|
|
1603
|
+
} else {
|
|
1604
|
+
agent.health.consecutiveFailures++;
|
|
1605
|
+
agent.health.status = agent.health.consecutiveFailures >= 3 ? "unhealthy" : "degraded";
|
|
1606
|
+
if (agent.state === "running" && agent.health.consecutiveFailures >= 2) {
|
|
1607
|
+
this.transition(agent, "degraded", `Health degraded: ${agent.health.consecutiveFailures} consecutive failures`, "system");
|
|
1608
|
+
}
|
|
1609
|
+
if (agent.health.consecutiveFailures >= 5 && agent.state !== "error") {
|
|
1610
|
+
this.emitEvent(agent, "auto_recovered", { action: "restart", failures: agent.health.consecutiveFailures });
|
|
1611
|
+
agent.health.consecutiveFailures = 0;
|
|
1612
|
+
try {
|
|
1613
|
+
await this.deployer.restart(agent.config);
|
|
1614
|
+
this.transition(agent, "starting", "Auto-restarted after health failures", "system");
|
|
1615
|
+
} catch {
|
|
1616
|
+
this.transition(agent, "error", "Auto-restart failed", "system");
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
agent.health.lastCheck = (/* @__PURE__ */ new Date()).toISOString();
|
|
1621
|
+
await this.persistAgent(agent);
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
agent.health.consecutiveFailures++;
|
|
1624
|
+
agent.health.status = "unhealthy";
|
|
1625
|
+
}
|
|
1626
|
+
}, 3e4);
|
|
1627
|
+
this.healthCheckIntervals.set(agent.id, interval);
|
|
1628
|
+
}
|
|
1629
|
+
stopHealthCheckLoop(agentId) {
|
|
1630
|
+
const interval = this.healthCheckIntervals.get(agentId);
|
|
1631
|
+
if (interval) {
|
|
1632
|
+
clearInterval(interval);
|
|
1633
|
+
this.healthCheckIntervals.delete(agentId);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
// ─── Private Helpers ──────────────────────────────────
|
|
1637
|
+
transition(agent, to, reason, triggeredBy) {
|
|
1638
|
+
const from = agent.state;
|
|
1639
|
+
agent.stateHistory.push({
|
|
1640
|
+
from,
|
|
1641
|
+
to,
|
|
1642
|
+
reason,
|
|
1643
|
+
triggeredBy,
|
|
1644
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1645
|
+
});
|
|
1646
|
+
if (agent.stateHistory.length > 50) agent.stateHistory = agent.stateHistory.slice(-50);
|
|
1647
|
+
agent.state = to;
|
|
1648
|
+
agent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1649
|
+
}
|
|
1650
|
+
isConfigComplete(config) {
|
|
1651
|
+
return !!(config.name && config.displayName && config.identity?.role && config.model?.modelId && config.deployment?.target && config.permissionProfileId);
|
|
1652
|
+
}
|
|
1653
|
+
async waitForHealthy(agent, timeoutMs) {
|
|
1654
|
+
const start = Date.now();
|
|
1655
|
+
while (Date.now() - start < timeoutMs) {
|
|
1656
|
+
try {
|
|
1657
|
+
const status = await this.deployer.getStatus(agent.config);
|
|
1658
|
+
if (status.status === "running") return true;
|
|
1659
|
+
} catch {
|
|
1660
|
+
}
|
|
1661
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
1662
|
+
}
|
|
1663
|
+
return false;
|
|
1664
|
+
}
|
|
1665
|
+
async persistAgent(agent) {
|
|
1666
|
+
this.agents.set(agent.id, agent);
|
|
1667
|
+
}
|
|
1668
|
+
emitEvent(agent, type, data) {
|
|
1669
|
+
const event = {
|
|
1670
|
+
id: crypto.randomUUID(),
|
|
1671
|
+
agentId: agent.id,
|
|
1672
|
+
orgId: agent.orgId,
|
|
1673
|
+
type,
|
|
1674
|
+
data,
|
|
1675
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1676
|
+
};
|
|
1677
|
+
for (const listener of this.eventListeners) {
|
|
1678
|
+
try {
|
|
1679
|
+
listener(event);
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
emptyUsage() {
|
|
1685
|
+
return {
|
|
1686
|
+
tokensToday: 0,
|
|
1687
|
+
tokensThisMonth: 0,
|
|
1688
|
+
tokenBudgetMonthly: 0,
|
|
1689
|
+
toolCallsToday: 0,
|
|
1690
|
+
toolCallsThisMonth: 0,
|
|
1691
|
+
externalActionsToday: 0,
|
|
1692
|
+
externalActionsThisMonth: 0,
|
|
1693
|
+
costToday: 0,
|
|
1694
|
+
costThisMonth: 0,
|
|
1695
|
+
costBudgetMonthly: 0,
|
|
1696
|
+
activeSessionCount: 0,
|
|
1697
|
+
totalSessionsToday: 0,
|
|
1698
|
+
errorsToday: 0,
|
|
1699
|
+
errorRate1h: 0,
|
|
1700
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Cleanup: stop all health check loops
|
|
1705
|
+
*/
|
|
1706
|
+
shutdown() {
|
|
1707
|
+
for (const [id] of this.healthCheckIntervals) {
|
|
1708
|
+
this.stopHealthCheckLoop(id);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
// src/engine/knowledge.ts
|
|
1714
|
+
var KnowledgeBaseEngine = class {
|
|
1715
|
+
knowledgeBases = /* @__PURE__ */ new Map();
|
|
1716
|
+
embeddings = /* @__PURE__ */ new Map();
|
|
1717
|
+
// chunkId → embedding
|
|
1718
|
+
/**
|
|
1719
|
+
* Create a new knowledge base
|
|
1720
|
+
*/
|
|
1721
|
+
createKnowledgeBase(orgId, opts) {
|
|
1722
|
+
const kb = {
|
|
1723
|
+
id: crypto.randomUUID(),
|
|
1724
|
+
orgId,
|
|
1725
|
+
name: opts.name,
|
|
1726
|
+
description: opts.description,
|
|
1727
|
+
agentIds: opts.agentIds || [],
|
|
1728
|
+
documents: [],
|
|
1729
|
+
stats: { totalDocuments: 0, totalChunks: 0, totalTokens: 0, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1730
|
+
config: {
|
|
1731
|
+
chunkSize: 512,
|
|
1732
|
+
chunkOverlap: 50,
|
|
1733
|
+
embeddingModel: "text-embedding-3-small",
|
|
1734
|
+
embeddingProvider: "openai",
|
|
1735
|
+
maxResultsPerQuery: 5,
|
|
1736
|
+
minSimilarityScore: 0.7,
|
|
1737
|
+
autoRefreshUrls: false,
|
|
1738
|
+
refreshIntervalHours: 24,
|
|
1739
|
+
...opts.config
|
|
1740
|
+
},
|
|
1741
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1742
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1743
|
+
};
|
|
1744
|
+
this.knowledgeBases.set(kb.id, kb);
|
|
1745
|
+
return kb;
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Ingest a document into a knowledge base
|
|
1749
|
+
*/
|
|
1750
|
+
async ingestDocument(kbId, opts) {
|
|
1751
|
+
const kb = this.knowledgeBases.get(kbId);
|
|
1752
|
+
if (!kb) throw new Error(`Knowledge base ${kbId} not found`);
|
|
1753
|
+
const doc = {
|
|
1754
|
+
id: crypto.randomUUID(),
|
|
1755
|
+
knowledgeBaseId: kbId,
|
|
1756
|
+
name: opts.name,
|
|
1757
|
+
sourceType: opts.sourceType,
|
|
1758
|
+
sourceUrl: opts.sourceUrl,
|
|
1759
|
+
mimeType: opts.mimeType || "text/plain",
|
|
1760
|
+
size: Buffer.byteLength(opts.content, "utf-8"),
|
|
1761
|
+
chunks: [],
|
|
1762
|
+
metadata: opts.metadata || {},
|
|
1763
|
+
status: "processing",
|
|
1764
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1765
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1766
|
+
};
|
|
1767
|
+
try {
|
|
1768
|
+
const text = this.extractText(opts.content, doc.mimeType);
|
|
1769
|
+
const chunks = this.chunkText(text, doc.id, kb.config);
|
|
1770
|
+
doc.chunks = chunks;
|
|
1771
|
+
if (kb.config.embeddingProvider !== "none") {
|
|
1772
|
+
await this.generateEmbeddings(chunks, kb.config);
|
|
1773
|
+
}
|
|
1774
|
+
doc.status = "ready";
|
|
1775
|
+
kb.documents.push(doc);
|
|
1776
|
+
kb.stats.totalDocuments = kb.documents.length;
|
|
1777
|
+
kb.stats.totalChunks = kb.documents.reduce((sum, d) => sum + d.chunks.length, 0);
|
|
1778
|
+
kb.stats.totalTokens = kb.documents.reduce((sum, d) => sum + d.chunks.reduce((cs, c) => cs + c.tokenCount, 0), 0);
|
|
1779
|
+
kb.stats.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1780
|
+
kb.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
doc.status = "error";
|
|
1783
|
+
doc.error = error.message;
|
|
1784
|
+
}
|
|
1785
|
+
return doc;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Search across knowledge bases for an agent
|
|
1789
|
+
*/
|
|
1790
|
+
async search(agentId, query, opts) {
|
|
1791
|
+
const kbs = Array.from(this.knowledgeBases.values()).filter((kb) => {
|
|
1792
|
+
if (opts?.kbIds?.length) return opts.kbIds.includes(kb.id);
|
|
1793
|
+
return kb.agentIds.includes(agentId) || kb.agentIds.length === 0;
|
|
1794
|
+
});
|
|
1795
|
+
if (kbs.length === 0) return [];
|
|
1796
|
+
const maxResults = opts?.maxResults || 5;
|
|
1797
|
+
const minScore = opts?.minScore || 0.7;
|
|
1798
|
+
const queryEmbedding = await this.getEmbedding(query, kbs[0].config);
|
|
1799
|
+
const results = [];
|
|
1800
|
+
for (const kb of kbs) {
|
|
1801
|
+
for (const doc of kb.documents) {
|
|
1802
|
+
if (doc.status !== "ready") continue;
|
|
1803
|
+
for (const chunk of doc.chunks) {
|
|
1804
|
+
let score;
|
|
1805
|
+
if (queryEmbedding && chunk.embedding) {
|
|
1806
|
+
score = this.cosineSimilarity(queryEmbedding, chunk.embedding);
|
|
1807
|
+
} else {
|
|
1808
|
+
score = this.keywordScore(query, chunk.content);
|
|
1809
|
+
}
|
|
1810
|
+
if (score >= minScore) {
|
|
1811
|
+
results.push({
|
|
1812
|
+
chunk,
|
|
1813
|
+
document: doc,
|
|
1814
|
+
score,
|
|
1815
|
+
highlight: this.extractHighlight(query, chunk.content)
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return results.sort((a, b) => b.score - a.score).slice(0, maxResults);
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Generate context string for an agent's prompt (RAG injection)
|
|
1825
|
+
*/
|
|
1826
|
+
async getContext(agentId, query, maxTokens = 2e3) {
|
|
1827
|
+
const results = await this.search(agentId, query);
|
|
1828
|
+
if (results.length === 0) return "";
|
|
1829
|
+
let context = "## Relevant Knowledge Base Context\n\n";
|
|
1830
|
+
let tokenCount = 0;
|
|
1831
|
+
for (const result of results) {
|
|
1832
|
+
const chunkTokens = result.chunk.tokenCount;
|
|
1833
|
+
if (tokenCount + chunkTokens > maxTokens) break;
|
|
1834
|
+
context += `### From: ${result.document.name}`;
|
|
1835
|
+
if (result.chunk.metadata.section) context += ` > ${result.chunk.metadata.section}`;
|
|
1836
|
+
context += `
|
|
1837
|
+
${result.chunk.content}
|
|
1838
|
+
|
|
1839
|
+
`;
|
|
1840
|
+
tokenCount += chunkTokens;
|
|
1841
|
+
}
|
|
1842
|
+
return context;
|
|
1843
|
+
}
|
|
1844
|
+
// ─── CRUD ───────────────────────────────────────────
|
|
1845
|
+
getKnowledgeBase(id) {
|
|
1846
|
+
return this.knowledgeBases.get(id);
|
|
1847
|
+
}
|
|
1848
|
+
getKnowledgeBasesByOrg(orgId) {
|
|
1849
|
+
return Array.from(this.knowledgeBases.values()).filter((kb) => kb.orgId === orgId);
|
|
1850
|
+
}
|
|
1851
|
+
getKnowledgeBasesForAgent(agentId) {
|
|
1852
|
+
return Array.from(this.knowledgeBases.values()).filter(
|
|
1853
|
+
(kb) => kb.agentIds.includes(agentId) || kb.agentIds.length === 0
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
deleteDocument(kbId, docId) {
|
|
1857
|
+
const kb = this.knowledgeBases.get(kbId);
|
|
1858
|
+
if (!kb) return false;
|
|
1859
|
+
const idx = kb.documents.findIndex((d) => d.id === docId);
|
|
1860
|
+
if (idx < 0) return false;
|
|
1861
|
+
for (const chunk of kb.documents[idx].chunks) {
|
|
1862
|
+
this.embeddings.delete(chunk.id);
|
|
1863
|
+
}
|
|
1864
|
+
kb.documents.splice(idx, 1);
|
|
1865
|
+
kb.stats.totalDocuments = kb.documents.length;
|
|
1866
|
+
kb.stats.totalChunks = kb.documents.reduce((sum, d) => sum + d.chunks.length, 0);
|
|
1867
|
+
kb.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1868
|
+
return true;
|
|
1869
|
+
}
|
|
1870
|
+
deleteKnowledgeBase(id) {
|
|
1871
|
+
return this.knowledgeBases.delete(id);
|
|
1872
|
+
}
|
|
1873
|
+
// ─── Text Processing ─────────────────────────────────
|
|
1874
|
+
extractText(content, mimeType) {
|
|
1875
|
+
switch (mimeType) {
|
|
1876
|
+
case "text/html":
|
|
1877
|
+
return content.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
1878
|
+
case "text/csv":
|
|
1879
|
+
return content.split("\n").map((row) => row.replace(/,/g, " | ")).join("\n");
|
|
1880
|
+
default:
|
|
1881
|
+
return content;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
chunkText(text, documentId, config) {
|
|
1885
|
+
const chunks = [];
|
|
1886
|
+
const sentences = this.splitIntoSentences(text);
|
|
1887
|
+
let currentChunk = "";
|
|
1888
|
+
let currentTokens = 0;
|
|
1889
|
+
let position = 0;
|
|
1890
|
+
let currentSection;
|
|
1891
|
+
for (const sentence of sentences) {
|
|
1892
|
+
const headingMatch = sentence.match(/^#+\s+(.+)$/);
|
|
1893
|
+
if (headingMatch) {
|
|
1894
|
+
currentSection = headingMatch[1];
|
|
1895
|
+
}
|
|
1896
|
+
const sentenceTokens = this.estimateTokens(sentence);
|
|
1897
|
+
if (currentTokens + sentenceTokens > config.chunkSize && currentChunk.length > 0) {
|
|
1898
|
+
chunks.push({
|
|
1899
|
+
id: crypto.randomUUID(),
|
|
1900
|
+
documentId,
|
|
1901
|
+
content: currentChunk.trim(),
|
|
1902
|
+
tokenCount: currentTokens,
|
|
1903
|
+
position: position++,
|
|
1904
|
+
metadata: { section: currentSection }
|
|
1905
|
+
});
|
|
1906
|
+
const overlapText = this.getOverlapText(currentChunk, config.chunkOverlap);
|
|
1907
|
+
currentChunk = overlapText + " " + sentence;
|
|
1908
|
+
currentTokens = this.estimateTokens(currentChunk);
|
|
1909
|
+
} else {
|
|
1910
|
+
currentChunk += " " + sentence;
|
|
1911
|
+
currentTokens += sentenceTokens;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (currentChunk.trim().length > 0) {
|
|
1915
|
+
chunks.push({
|
|
1916
|
+
id: crypto.randomUUID(),
|
|
1917
|
+
documentId,
|
|
1918
|
+
content: currentChunk.trim(),
|
|
1919
|
+
tokenCount: currentTokens,
|
|
1920
|
+
position,
|
|
1921
|
+
metadata: { section: currentSection }
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
return chunks;
|
|
1925
|
+
}
|
|
1926
|
+
splitIntoSentences(text) {
|
|
1927
|
+
return text.split(/(?<=[.!?])\s+|(?=^#+\s)/m).filter((s) => s.trim().length > 0);
|
|
1928
|
+
}
|
|
1929
|
+
estimateTokens(text) {
|
|
1930
|
+
return Math.ceil(text.length / 4);
|
|
1931
|
+
}
|
|
1932
|
+
getOverlapText(text, overlapTokens) {
|
|
1933
|
+
const words = text.split(/\s+/);
|
|
1934
|
+
const overlapWords = Math.ceil(overlapTokens * 0.75);
|
|
1935
|
+
return words.slice(-overlapWords).join(" ");
|
|
1936
|
+
}
|
|
1937
|
+
// ─── Embeddings ─────────────────────────────────────
|
|
1938
|
+
async generateEmbeddings(chunks, config) {
|
|
1939
|
+
if (config.embeddingProvider === "openai") {
|
|
1940
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
1941
|
+
if (!apiKey) return;
|
|
1942
|
+
const batchSize = 100;
|
|
1943
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
1944
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
1945
|
+
try {
|
|
1946
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
1947
|
+
method: "POST",
|
|
1948
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
1949
|
+
body: JSON.stringify({
|
|
1950
|
+
model: config.embeddingModel,
|
|
1951
|
+
input: batch.map((c) => c.content)
|
|
1952
|
+
})
|
|
1953
|
+
});
|
|
1954
|
+
if (response.ok) {
|
|
1955
|
+
const data = await response.json();
|
|
1956
|
+
for (let j = 0; j < data.data.length; j++) {
|
|
1957
|
+
batch[j].embedding = data.data[j].embedding;
|
|
1958
|
+
this.embeddings.set(batch[j].id, data.data[j].embedding);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
async getEmbedding(text, config) {
|
|
1967
|
+
if (config.embeddingProvider !== "openai") return null;
|
|
1968
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
1969
|
+
if (!apiKey) return null;
|
|
1970
|
+
try {
|
|
1971
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
1972
|
+
method: "POST",
|
|
1973
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
1974
|
+
body: JSON.stringify({ model: config.embeddingModel, input: text })
|
|
1975
|
+
});
|
|
1976
|
+
if (response.ok) {
|
|
1977
|
+
const data = await response.json();
|
|
1978
|
+
return data.data[0].embedding;
|
|
1979
|
+
}
|
|
1980
|
+
} catch {
|
|
1981
|
+
}
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
cosineSimilarity(a, b) {
|
|
1985
|
+
if (a.length !== b.length) return 0;
|
|
1986
|
+
let dotProduct = 0, normA = 0, normB = 0;
|
|
1987
|
+
for (let i = 0; i < a.length; i++) {
|
|
1988
|
+
dotProduct += a[i] * b[i];
|
|
1989
|
+
normA += a[i] * a[i];
|
|
1990
|
+
normB += b[i] * b[i];
|
|
1991
|
+
}
|
|
1992
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1993
|
+
return denominator === 0 ? 0 : dotProduct / denominator;
|
|
1994
|
+
}
|
|
1995
|
+
keywordScore(query, content) {
|
|
1996
|
+
const queryWords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
1997
|
+
const contentLower = content.toLowerCase();
|
|
1998
|
+
let matches = 0;
|
|
1999
|
+
for (const word of queryWords) {
|
|
2000
|
+
if (contentLower.includes(word)) matches++;
|
|
2001
|
+
}
|
|
2002
|
+
return queryWords.length > 0 ? matches / queryWords.length : 0;
|
|
2003
|
+
}
|
|
2004
|
+
extractHighlight(query, content, maxLength = 200) {
|
|
2005
|
+
const queryWords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
2006
|
+
const sentences = content.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
2007
|
+
let bestSentence = sentences[0] || content.slice(0, maxLength);
|
|
2008
|
+
let bestScore = 0;
|
|
2009
|
+
for (const sentence of sentences) {
|
|
2010
|
+
const lower = sentence.toLowerCase();
|
|
2011
|
+
const score = queryWords.filter((w) => lower.includes(w)).length;
|
|
2012
|
+
if (score > bestScore) {
|
|
2013
|
+
bestScore = score;
|
|
2014
|
+
bestSentence = sentence;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return bestSentence.trim().slice(0, maxLength) + (bestSentence.length > maxLength ? "..." : "");
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
// src/engine/tenant.ts
|
|
2022
|
+
var PLAN_LIMITS = {
|
|
2023
|
+
free: {
|
|
2024
|
+
maxAgents: 3,
|
|
2025
|
+
maxUsers: 5,
|
|
2026
|
+
maxKnowledgeBases: 1,
|
|
2027
|
+
maxDocumentsPerKB: 10,
|
|
2028
|
+
maxStorageMb: 100,
|
|
2029
|
+
tokenBudgetMonthly: 1e6,
|
|
2030
|
+
apiCallsPerMinute: 30,
|
|
2031
|
+
deploymentTargets: ["docker", "local"],
|
|
2032
|
+
features: ["knowledge-base"]
|
|
2033
|
+
},
|
|
2034
|
+
team: {
|
|
2035
|
+
maxAgents: 25,
|
|
2036
|
+
maxUsers: 50,
|
|
2037
|
+
maxKnowledgeBases: 10,
|
|
2038
|
+
maxDocumentsPerKB: 100,
|
|
2039
|
+
maxStorageMb: 5e3,
|
|
2040
|
+
tokenBudgetMonthly: 1e7,
|
|
2041
|
+
apiCallsPerMinute: 120,
|
|
2042
|
+
deploymentTargets: ["docker", "vps", "fly", "railway", "local"],
|
|
2043
|
+
features: ["knowledge-base", "api-access", "webhooks", "approval-workflows", "sso", "audit-export", "custom-skills"]
|
|
2044
|
+
},
|
|
2045
|
+
enterprise: {
|
|
2046
|
+
maxAgents: 999999,
|
|
2047
|
+
maxUsers: 999999,
|
|
2048
|
+
maxKnowledgeBases: 999,
|
|
2049
|
+
maxDocumentsPerKB: 1e4,
|
|
2050
|
+
maxStorageMb: 1e5,
|
|
2051
|
+
tokenBudgetMonthly: 0,
|
|
2052
|
+
// Unlimited
|
|
2053
|
+
apiCallsPerMinute: 600,
|
|
2054
|
+
deploymentTargets: ["docker", "vps", "fly", "railway", "aws", "gcp", "azure", "local"],
|
|
2055
|
+
features: ["knowledge-base", "api-access", "webhooks", "approval-workflows", "sso", "audit-export", "custom-skills", "custom-domain", "priority-support", "sla", "data-residency", "ip-allowlist", "multi-deploy", "white-label"]
|
|
2056
|
+
},
|
|
2057
|
+
"self-hosted": {
|
|
2058
|
+
maxAgents: 999999,
|
|
2059
|
+
maxUsers: 999999,
|
|
2060
|
+
maxKnowledgeBases: 999,
|
|
2061
|
+
maxDocumentsPerKB: 1e4,
|
|
2062
|
+
maxStorageMb: 999999,
|
|
2063
|
+
tokenBudgetMonthly: 0,
|
|
2064
|
+
apiCallsPerMinute: 999,
|
|
2065
|
+
deploymentTargets: ["docker", "vps", "fly", "railway", "aws", "gcp", "azure", "local"],
|
|
2066
|
+
features: ["knowledge-base", "api-access", "webhooks", "approval-workflows", "sso", "audit-export", "custom-skills", "custom-domain", "data-residency", "ip-allowlist", "multi-deploy", "white-label"]
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
var TenantManager = class {
|
|
2070
|
+
orgs = /* @__PURE__ */ new Map();
|
|
2071
|
+
/**
|
|
2072
|
+
* Create a new organization
|
|
2073
|
+
*/
|
|
2074
|
+
createOrg(opts) {
|
|
2075
|
+
if (this.orgs.has(opts.slug)) {
|
|
2076
|
+
throw new Error(`Organization slug "${opts.slug}" already exists`);
|
|
2077
|
+
}
|
|
2078
|
+
const limits = { ...PLAN_LIMITS[opts.plan] };
|
|
2079
|
+
const org = {
|
|
2080
|
+
id: crypto.randomUUID(),
|
|
2081
|
+
name: opts.name,
|
|
2082
|
+
slug: opts.slug,
|
|
2083
|
+
plan: opts.plan,
|
|
2084
|
+
limits,
|
|
2085
|
+
usage: {
|
|
2086
|
+
agents: 0,
|
|
2087
|
+
users: 1,
|
|
2088
|
+
tokensThisMonth: 0,
|
|
2089
|
+
costThisMonth: 0,
|
|
2090
|
+
storageMb: 0,
|
|
2091
|
+
apiCallsToday: 0,
|
|
2092
|
+
deploymentsThisMonth: 0,
|
|
2093
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2094
|
+
},
|
|
2095
|
+
allowedDomains: [],
|
|
2096
|
+
settings: {
|
|
2097
|
+
defaultModel: "anthropic/claude-sonnet-4-20250514",
|
|
2098
|
+
defaultPermissionProfile: "Customer Support Agent",
|
|
2099
|
+
requireApprovalForDeploy: true,
|
|
2100
|
+
auditRetentionDays: opts.plan === "free" ? 30 : opts.plan === "team" ? 90 : 365,
|
|
2101
|
+
dataRegion: "us-east-1",
|
|
2102
|
+
...opts.settings
|
|
2103
|
+
},
|
|
2104
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2105
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2106
|
+
};
|
|
2107
|
+
this.orgs.set(org.id, org);
|
|
2108
|
+
return org;
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Check if an org can perform an action (within limits)
|
|
2112
|
+
*/
|
|
2113
|
+
checkLimit(orgId, resource, currentCount) {
|
|
2114
|
+
const org = this.orgs.get(orgId);
|
|
2115
|
+
if (!org) throw new Error(`Organization ${orgId} not found`);
|
|
2116
|
+
const limit = org.limits[resource];
|
|
2117
|
+
if (typeof limit !== "number") return { allowed: true, limit: 0, current: 0, remaining: 0 };
|
|
2118
|
+
let current = currentCount ?? 0;
|
|
2119
|
+
if (!currentCount) {
|
|
2120
|
+
switch (resource) {
|
|
2121
|
+
case "maxAgents":
|
|
2122
|
+
current = org.usage.agents;
|
|
2123
|
+
break;
|
|
2124
|
+
case "maxUsers":
|
|
2125
|
+
current = org.usage.users;
|
|
2126
|
+
break;
|
|
2127
|
+
case "tokenBudgetMonthly":
|
|
2128
|
+
current = org.usage.tokensThisMonth;
|
|
2129
|
+
break;
|
|
2130
|
+
case "maxStorageMb":
|
|
2131
|
+
current = org.usage.storageMb;
|
|
2132
|
+
break;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
const remaining = Math.max(0, limit - current);
|
|
2136
|
+
return {
|
|
2137
|
+
allowed: limit === 0 || current < limit,
|
|
2138
|
+
// 0 = unlimited
|
|
2139
|
+
limit,
|
|
2140
|
+
current,
|
|
2141
|
+
remaining
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Check if an org has a specific feature
|
|
2146
|
+
*/
|
|
2147
|
+
hasFeature(orgId, feature) {
|
|
2148
|
+
const org = this.orgs.get(orgId);
|
|
2149
|
+
if (!org) return false;
|
|
2150
|
+
return org.limits.features.includes(feature);
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Check if a deployment target is allowed for this org's plan
|
|
2154
|
+
*/
|
|
2155
|
+
canDeployTo(orgId, target) {
|
|
2156
|
+
const org = this.orgs.get(orgId);
|
|
2157
|
+
if (!org) return false;
|
|
2158
|
+
return org.limits.deploymentTargets.includes(target);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Record usage (tokens, API calls, etc.)
|
|
2162
|
+
*/
|
|
2163
|
+
recordUsage(orgId, update) {
|
|
2164
|
+
const org = this.orgs.get(orgId);
|
|
2165
|
+
if (!org) return;
|
|
2166
|
+
if (update.tokensThisMonth) org.usage.tokensThisMonth += update.tokensThisMonth;
|
|
2167
|
+
if (update.costThisMonth) org.usage.costThisMonth += update.costThisMonth;
|
|
2168
|
+
if (update.apiCallsToday) org.usage.apiCallsToday += update.apiCallsToday;
|
|
2169
|
+
if (update.storageMb) org.usage.storageMb = update.storageMb;
|
|
2170
|
+
if (update.deploymentsThisMonth) org.usage.deploymentsThisMonth += update.deploymentsThisMonth;
|
|
2171
|
+
org.usage.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Upgrade/downgrade org plan
|
|
2175
|
+
*/
|
|
2176
|
+
changePlan(orgId, newPlan) {
|
|
2177
|
+
const org = this.orgs.get(orgId);
|
|
2178
|
+
if (!org) throw new Error(`Organization ${orgId} not found`);
|
|
2179
|
+
org.plan = newPlan;
|
|
2180
|
+
org.limits = { ...PLAN_LIMITS[newPlan] };
|
|
2181
|
+
org.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2182
|
+
org.settings.auditRetentionDays = newPlan === "free" ? 30 : newPlan === "team" ? 90 : 365;
|
|
2183
|
+
return org;
|
|
2184
|
+
}
|
|
2185
|
+
getOrg(id) {
|
|
2186
|
+
return this.orgs.get(id);
|
|
2187
|
+
}
|
|
2188
|
+
getOrgBySlug(slug) {
|
|
2189
|
+
return Array.from(this.orgs.values()).find((o) => o.slug === slug);
|
|
2190
|
+
}
|
|
2191
|
+
listOrgs() {
|
|
2192
|
+
return Array.from(this.orgs.values());
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Single-tenant mode: create default org with unlimited (self-hosted) plan.
|
|
2196
|
+
* For open-source / self-hosted deployments that don't need multi-tenancy.
|
|
2197
|
+
*/
|
|
2198
|
+
createDefaultOrg(name = "Default") {
|
|
2199
|
+
const existing = this.getOrgBySlug("default");
|
|
2200
|
+
if (existing) return existing;
|
|
2201
|
+
return this.createOrg({
|
|
2202
|
+
name,
|
|
2203
|
+
slug: "default",
|
|
2204
|
+
plan: "self-hosted",
|
|
2205
|
+
adminEmail: "admin@localhost",
|
|
2206
|
+
settings: { requireApprovalForDeploy: false }
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Is this a single-tenant deployment?
|
|
2211
|
+
*/
|
|
2212
|
+
isSingleTenant() {
|
|
2213
|
+
const orgs = this.listOrgs();
|
|
2214
|
+
return orgs.length === 1 && orgs[0].slug === "default";
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Reset daily counters for all orgs
|
|
2218
|
+
*/
|
|
2219
|
+
resetDailyCounters() {
|
|
2220
|
+
for (const org of this.orgs.values()) {
|
|
2221
|
+
org.usage.apiCallsToday = 0;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Reset monthly counters for all orgs
|
|
2226
|
+
*/
|
|
2227
|
+
resetMonthlyCounters() {
|
|
2228
|
+
for (const org of this.orgs.values()) {
|
|
2229
|
+
org.usage.tokensThisMonth = 0;
|
|
2230
|
+
org.usage.costThisMonth = 0;
|
|
2231
|
+
org.usage.deploymentsThisMonth = 0;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
|
|
2236
|
+
// src/engine/activity.ts
|
|
2237
|
+
var ActivityTracker = class {
|
|
2238
|
+
events = [];
|
|
2239
|
+
toolCalls = /* @__PURE__ */ new Map();
|
|
2240
|
+
conversations = [];
|
|
2241
|
+
listeners = [];
|
|
2242
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
2243
|
+
// Buffer settings
|
|
2244
|
+
maxEvents = 1e4;
|
|
2245
|
+
// Keep last N events in memory
|
|
2246
|
+
maxToolCalls = 5e3;
|
|
2247
|
+
maxConversations = 5e3;
|
|
2248
|
+
// ─── Record Events ───────────────────────────────────
|
|
2249
|
+
/**
|
|
2250
|
+
* Record a generic activity event
|
|
2251
|
+
*/
|
|
2252
|
+
record(event) {
|
|
2253
|
+
const full = {
|
|
2254
|
+
...event,
|
|
2255
|
+
id: crypto.randomUUID(),
|
|
2256
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2257
|
+
};
|
|
2258
|
+
this.events.push(full);
|
|
2259
|
+
if (this.events.length > this.maxEvents) {
|
|
2260
|
+
this.events = this.events.slice(-this.maxEvents);
|
|
2261
|
+
}
|
|
2262
|
+
for (const listener of this.listeners) {
|
|
2263
|
+
try {
|
|
2264
|
+
listener(full);
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
for (const client of this.sseClients) {
|
|
2269
|
+
try {
|
|
2270
|
+
client(full);
|
|
2271
|
+
} catch {
|
|
2272
|
+
this.sseClients.delete(client);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return full;
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Record a tool call starting
|
|
2279
|
+
*/
|
|
2280
|
+
startToolCall(opts) {
|
|
2281
|
+
const record = {
|
|
2282
|
+
id: crypto.randomUUID(),
|
|
2283
|
+
agentId: opts.agentId,
|
|
2284
|
+
orgId: opts.orgId,
|
|
2285
|
+
sessionId: opts.sessionId,
|
|
2286
|
+
toolId: opts.toolId,
|
|
2287
|
+
toolName: opts.toolName,
|
|
2288
|
+
parameters: this.sanitizeParams(opts.parameters),
|
|
2289
|
+
timing: { startedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2290
|
+
permission: opts.permission
|
|
2291
|
+
};
|
|
2292
|
+
this.toolCalls.set(record.id, record);
|
|
2293
|
+
if (this.toolCalls.size > this.maxToolCalls) {
|
|
2294
|
+
const keys = Array.from(this.toolCalls.keys());
|
|
2295
|
+
for (let i = 0; i < 1e3; i++) this.toolCalls.delete(keys[i]);
|
|
2296
|
+
}
|
|
2297
|
+
this.record({
|
|
2298
|
+
agentId: opts.agentId,
|
|
2299
|
+
orgId: opts.orgId,
|
|
2300
|
+
sessionId: opts.sessionId,
|
|
2301
|
+
type: opts.permission.allowed ? "tool_call_start" : "tool_blocked",
|
|
2302
|
+
data: { toolCallId: record.id, toolId: opts.toolId, toolName: opts.toolName }
|
|
2303
|
+
});
|
|
2304
|
+
return record;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Record a tool call completing
|
|
2308
|
+
*/
|
|
2309
|
+
endToolCall(toolCallId, result) {
|
|
2310
|
+
const record = this.toolCalls.get(toolCallId);
|
|
2311
|
+
if (!record) return;
|
|
2312
|
+
record.timing.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2313
|
+
record.timing.durationMs = new Date(record.timing.completedAt).getTime() - new Date(record.timing.startedAt).getTime();
|
|
2314
|
+
record.result = {
|
|
2315
|
+
success: result.success,
|
|
2316
|
+
truncatedOutput: result.output?.slice(0, 500),
|
|
2317
|
+
error: result.error
|
|
2318
|
+
};
|
|
2319
|
+
if (result.inputTokens || result.outputTokens || result.costUsd) {
|
|
2320
|
+
record.cost = {
|
|
2321
|
+
inputTokens: result.inputTokens || 0,
|
|
2322
|
+
outputTokens: result.outputTokens || 0,
|
|
2323
|
+
estimatedCostUsd: result.costUsd || 0
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
this.record({
|
|
2327
|
+
agentId: record.agentId,
|
|
2328
|
+
orgId: record.orgId,
|
|
2329
|
+
sessionId: record.sessionId,
|
|
2330
|
+
type: result.success ? "tool_call_end" : "tool_call_error",
|
|
2331
|
+
data: {
|
|
2332
|
+
toolCallId,
|
|
2333
|
+
toolId: record.toolId,
|
|
2334
|
+
durationMs: record.timing.durationMs,
|
|
2335
|
+
success: result.success,
|
|
2336
|
+
error: result.error
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Record a conversation message
|
|
2342
|
+
*/
|
|
2343
|
+
recordMessage(entry) {
|
|
2344
|
+
const full = {
|
|
2345
|
+
...entry,
|
|
2346
|
+
id: crypto.randomUUID(),
|
|
2347
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2348
|
+
content: entry.content.slice(0, 2e3)
|
|
2349
|
+
// Truncate
|
|
2350
|
+
};
|
|
2351
|
+
this.conversations.push(full);
|
|
2352
|
+
if (this.conversations.length > this.maxConversations) {
|
|
2353
|
+
this.conversations = this.conversations.slice(-this.maxConversations);
|
|
2354
|
+
}
|
|
2355
|
+
this.record({
|
|
2356
|
+
agentId: entry.agentId,
|
|
2357
|
+
orgId: "",
|
|
2358
|
+
sessionId: entry.sessionId,
|
|
2359
|
+
type: entry.role === "user" ? "message_received" : "message_sent",
|
|
2360
|
+
data: { messageId: full.id, role: entry.role, channel: entry.channel, tokenCount: entry.tokenCount }
|
|
2361
|
+
});
|
|
2362
|
+
return full;
|
|
2363
|
+
}
|
|
2364
|
+
// ─── Query ──────────────────────────────────────────
|
|
2365
|
+
/**
|
|
2366
|
+
* Get recent events for an agent
|
|
2367
|
+
*/
|
|
2368
|
+
getEvents(opts) {
|
|
2369
|
+
let events = [...this.events];
|
|
2370
|
+
if (opts.agentId) events = events.filter((e) => e.agentId === opts.agentId);
|
|
2371
|
+
if (opts.orgId) events = events.filter((e) => e.orgId === opts.orgId);
|
|
2372
|
+
if (opts.types?.length) events = events.filter((e) => opts.types.includes(e.type));
|
|
2373
|
+
if (opts.since) {
|
|
2374
|
+
const sinceTs = new Date(opts.since).getTime();
|
|
2375
|
+
events = events.filter((e) => new Date(e.timestamp).getTime() >= sinceTs);
|
|
2376
|
+
}
|
|
2377
|
+
events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
2378
|
+
return events.slice(0, opts.limit || 50);
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Get tool call history for an agent
|
|
2382
|
+
*/
|
|
2383
|
+
getToolCalls(opts) {
|
|
2384
|
+
let calls = Array.from(this.toolCalls.values());
|
|
2385
|
+
if (opts.agentId) calls = calls.filter((c) => c.agentId === opts.agentId);
|
|
2386
|
+
if (opts.orgId) calls = calls.filter((c) => c.orgId === opts.orgId);
|
|
2387
|
+
if (opts.toolId) calls = calls.filter((c) => c.toolId === opts.toolId);
|
|
2388
|
+
calls.sort((a, b) => new Date(b.timing.startedAt).getTime() - new Date(a.timing.startedAt).getTime());
|
|
2389
|
+
return calls.slice(0, opts.limit || 50);
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Get conversation history for a session
|
|
2393
|
+
*/
|
|
2394
|
+
getConversation(sessionId, limit = 50) {
|
|
2395
|
+
return this.conversations.filter((c) => c.sessionId === sessionId).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()).slice(-limit);
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Generate a daily timeline for an agent
|
|
2399
|
+
*/
|
|
2400
|
+
getTimeline(agentId, date) {
|
|
2401
|
+
const dayStart = /* @__PURE__ */ new Date(date + "T00:00:00Z");
|
|
2402
|
+
const dayEnd = /* @__PURE__ */ new Date(date + "T23:59:59Z");
|
|
2403
|
+
const dayEvents = this.events.filter((e) => {
|
|
2404
|
+
if (e.agentId !== agentId) return false;
|
|
2405
|
+
const ts = new Date(e.timestamp).getTime();
|
|
2406
|
+
return ts >= dayStart.getTime() && ts <= dayEnd.getTime();
|
|
2407
|
+
});
|
|
2408
|
+
const dayToolCalls = Array.from(this.toolCalls.values()).filter((tc) => {
|
|
2409
|
+
if (tc.agentId !== agentId) return false;
|
|
2410
|
+
const ts = new Date(tc.timing.startedAt).getTime();
|
|
2411
|
+
return ts >= dayStart.getTime() && ts <= dayEnd.getTime();
|
|
2412
|
+
});
|
|
2413
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
2414
|
+
let totalTokens = 0, totalCost = 0, totalErrors = 0;
|
|
2415
|
+
const activeHours = /* @__PURE__ */ new Set();
|
|
2416
|
+
for (const tc of dayToolCalls) {
|
|
2417
|
+
toolCounts.set(tc.toolId, (toolCounts.get(tc.toolId) || 0) + 1);
|
|
2418
|
+
if (tc.cost) {
|
|
2419
|
+
totalTokens += tc.cost.inputTokens + tc.cost.outputTokens;
|
|
2420
|
+
totalCost += tc.cost.estimatedCostUsd;
|
|
2421
|
+
}
|
|
2422
|
+
if (tc.result && !tc.result.success) totalErrors++;
|
|
2423
|
+
activeHours.add(new Date(tc.timing.startedAt).getUTCHours());
|
|
2424
|
+
}
|
|
2425
|
+
const topTools = Array.from(toolCounts.entries()).map(([toolId, count]) => ({ toolId, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
2426
|
+
const totalMessages = dayEvents.filter(
|
|
2427
|
+
(e) => e.type === "message_received" || e.type === "message_sent"
|
|
2428
|
+
).length;
|
|
2429
|
+
const totalSessions = new Set(dayEvents.filter((e) => e.sessionId).map((e) => e.sessionId)).size;
|
|
2430
|
+
return {
|
|
2431
|
+
agentId,
|
|
2432
|
+
date,
|
|
2433
|
+
events: dayEvents.map((e) => ({
|
|
2434
|
+
timestamp: e.timestamp,
|
|
2435
|
+
type: e.type,
|
|
2436
|
+
summary: this.summarizeEvent(e),
|
|
2437
|
+
details: e.data
|
|
2438
|
+
})),
|
|
2439
|
+
summary: {
|
|
2440
|
+
totalSessions,
|
|
2441
|
+
totalMessages,
|
|
2442
|
+
totalToolCalls: dayToolCalls.length,
|
|
2443
|
+
totalErrors,
|
|
2444
|
+
totalTokens,
|
|
2445
|
+
totalCostUsd: totalCost,
|
|
2446
|
+
topTools,
|
|
2447
|
+
activeHours: Array.from(activeHours).sort((a, b) => a - b)
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
// ─── Real-time Streaming (SSE) ────────────────────────
|
|
2452
|
+
/**
|
|
2453
|
+
* Subscribe to real-time events (for SSE endpoint)
|
|
2454
|
+
*/
|
|
2455
|
+
subscribe(callback) {
|
|
2456
|
+
this.sseClients.add(callback);
|
|
2457
|
+
return () => this.sseClients.delete(callback);
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Subscribe to events (general listener)
|
|
2461
|
+
*/
|
|
2462
|
+
onEvent(listener) {
|
|
2463
|
+
this.listeners.push(listener);
|
|
2464
|
+
return () => {
|
|
2465
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
// ─── Stats ──────────────────────────────────────────
|
|
2469
|
+
/**
|
|
2470
|
+
* Get real-time stats for dashboard
|
|
2471
|
+
*/
|
|
2472
|
+
getStats(orgId) {
|
|
2473
|
+
const fiveMinAgo = new Date(Date.now() - 5 * 6e4).toISOString();
|
|
2474
|
+
let recent = this.events.filter((e) => e.timestamp >= fiveMinAgo);
|
|
2475
|
+
if (orgId) recent = recent.filter((e) => e.orgId === orgId);
|
|
2476
|
+
const toolCalls = recent.filter((e) => e.type === "tool_call_start" || e.type === "tool_call_end");
|
|
2477
|
+
const errors = recent.filter((e) => e.type === "tool_call_error" || e.type === "error");
|
|
2478
|
+
const activeAgents = [...new Set(recent.map((e) => e.agentId))];
|
|
2479
|
+
const activeSessions = new Set(recent.filter((e) => e.sessionId).map((e) => e.sessionId)).size;
|
|
2480
|
+
return {
|
|
2481
|
+
eventsLast5min: recent.length,
|
|
2482
|
+
toolCallsLast5min: toolCalls.length,
|
|
2483
|
+
errorsLast5min: errors.length,
|
|
2484
|
+
activeAgents,
|
|
2485
|
+
activeSessions
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
// ─── Private ──────────────────────────────────────────
|
|
2489
|
+
sanitizeParams(params) {
|
|
2490
|
+
const sanitized = { ...params };
|
|
2491
|
+
const sensitiveKeys = ["password", "token", "secret", "key", "apiKey", "credential", "authorization"];
|
|
2492
|
+
for (const key of Object.keys(sanitized)) {
|
|
2493
|
+
if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk))) {
|
|
2494
|
+
sanitized[key] = "***";
|
|
2495
|
+
}
|
|
2496
|
+
if (typeof sanitized[key] === "string" && sanitized[key].length > 200) {
|
|
2497
|
+
sanitized[key] = sanitized[key].slice(0, 200) + "...";
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
return sanitized;
|
|
2501
|
+
}
|
|
2502
|
+
summarizeEvent(event) {
|
|
2503
|
+
switch (event.type) {
|
|
2504
|
+
case "session_start":
|
|
2505
|
+
return "Session started";
|
|
2506
|
+
case "session_end":
|
|
2507
|
+
return "Session ended";
|
|
2508
|
+
case "message_received":
|
|
2509
|
+
return `Received message via ${event.data.channel || "unknown"}`;
|
|
2510
|
+
case "message_sent":
|
|
2511
|
+
return `Sent reply via ${event.data.channel || "unknown"}`;
|
|
2512
|
+
case "tool_call_start":
|
|
2513
|
+
return `Called ${event.data.toolName || event.data.toolId}`;
|
|
2514
|
+
case "tool_call_end":
|
|
2515
|
+
return `${event.data.toolName || event.data.toolId} completed (${event.data.durationMs}ms)`;
|
|
2516
|
+
case "tool_call_error":
|
|
2517
|
+
return `${event.data.toolName || event.data.toolId} failed: ${event.data.error}`;
|
|
2518
|
+
case "tool_blocked":
|
|
2519
|
+
return `Blocked: ${event.data.toolName || event.data.toolId}`;
|
|
2520
|
+
case "email_received":
|
|
2521
|
+
return "Received email";
|
|
2522
|
+
case "email_sent":
|
|
2523
|
+
return "Sent email";
|
|
2524
|
+
case "error":
|
|
2525
|
+
return `Error: ${event.data.message || "Unknown"}`;
|
|
2526
|
+
case "heartbeat":
|
|
2527
|
+
return "Heartbeat check";
|
|
2528
|
+
default:
|
|
2529
|
+
return event.type;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
};
|
|
2533
|
+
|
|
2534
|
+
export {
|
|
2535
|
+
OPENCLAW_CORE_TOOLS,
|
|
2536
|
+
AGENTICMAIL_TOOLS,
|
|
2537
|
+
ALL_TOOLS,
|
|
2538
|
+
TOOL_INDEX,
|
|
2539
|
+
getToolsBySkill,
|
|
2540
|
+
generateOpenClawToolPolicy,
|
|
2541
|
+
init_tool_catalog,
|
|
2542
|
+
PRESET_PROFILES,
|
|
2543
|
+
BUILTIN_SKILLS,
|
|
2544
|
+
PermissionEngine,
|
|
2545
|
+
AgentConfigGenerator,
|
|
2546
|
+
DeploymentEngine,
|
|
2547
|
+
ApprovalEngine,
|
|
2548
|
+
AgentLifecycleManager,
|
|
2549
|
+
KnowledgeBaseEngine,
|
|
2550
|
+
PLAN_LIMITS,
|
|
2551
|
+
TenantManager,
|
|
2552
|
+
ActivityTracker
|
|
2553
|
+
};
|