@agenticmail/enterprise 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. package/tsconfig.json +14 -0
@@ -0,0 +1,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
+ };