@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,473 @@
1
+ /**
2
+ * Skill & Tool Registry + Permission Engine
3
+ *
4
+ * This is the core of enterprise: companies control exactly what their
5
+ * AI agents can and cannot do. Every skill and tool is cataloged,
6
+ * categorized, and gated behind permissions.
7
+ */
8
+
9
+ // ─── Types ──────────────────────────────────────────────
10
+
11
+ export interface SkillDefinition {
12
+ id: string; // e.g. "github", "imsg", "browser"
13
+ name: string; // Human-readable: "GitHub"
14
+ description: string; // What it does
15
+ category: SkillCategory;
16
+ risk: RiskLevel; // How dangerous is this skill
17
+ tools: ToolDefinition[]; // Tools this skill provides
18
+ requires?: string[]; // System requirements (e.g. "macos", "docker", "node")
19
+ configSchema?: Record<string, ConfigField>; // Skill-specific config fields
20
+ icon?: string; // Emoji or icon URL
21
+ source?: 'builtin' | 'clawhub' | 'custom';
22
+ version?: string;
23
+ author?: string;
24
+ }
25
+
26
+ export interface ToolDefinition {
27
+ id: string; // e.g. "exec", "web_search", "agenticmail_send"
28
+ name: string;
29
+ description: string;
30
+ category: ToolCategory;
31
+ risk: RiskLevel;
32
+ skillId: string; // Parent skill
33
+ parameters?: Record<string, any>;
34
+ sideEffects: SideEffect[]; // What external effects can this tool cause
35
+ }
36
+
37
+ export interface ConfigField {
38
+ type: 'string' | 'number' | 'boolean' | 'select' | 'secret';
39
+ label: string;
40
+ description?: string;
41
+ required?: boolean;
42
+ default?: any;
43
+ options?: { label: string; value: string }[]; // For select type
44
+ }
45
+
46
+ export type SkillCategory =
47
+ | 'communication' // Email, SMS, messaging
48
+ | 'development' // GitHub, coding, git
49
+ | 'productivity' // Calendar, notes, reminders, tasks
50
+ | 'research' // Web search, web fetch, summarize
51
+ | 'media' // Image gen, TTS, video, audio
52
+ | 'automation' // Browser, shell, scripting
53
+ | 'smart-home' // Hue, Sonos, cameras, Eight Sleep
54
+ | 'data' // Files, databases, storage
55
+ | 'security' // 1Password, healthcheck
56
+ | 'social' // Twitter/X, social media
57
+ | 'platform'; // Core OpenClaw/AgenticMail internals
58
+
59
+ export type ToolCategory =
60
+ | 'read' // Read-only, no side effects
61
+ | 'write' // Creates or modifies data
62
+ | 'execute' // Runs code/commands
63
+ | 'communicate' // Sends messages externally
64
+ | 'destroy'; // Deletes data
65
+
66
+ export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
67
+
68
+ export type SideEffect =
69
+ | 'sends-email'
70
+ | 'sends-message'
71
+ | 'sends-sms'
72
+ | 'posts-social'
73
+ | 'runs-code'
74
+ | 'modifies-files'
75
+ | 'deletes-data'
76
+ | 'network-request'
77
+ | 'controls-device'
78
+ | 'accesses-secrets'
79
+ | 'financial';
80
+
81
+ // ─── Agent Permission Profile ───────────────────────────
82
+
83
+ export interface AgentPermissionProfile {
84
+ id: string;
85
+ name: string; // e.g. "Customer Support Agent", "Research Agent"
86
+ description?: string;
87
+
88
+ // Skill-level controls
89
+ skills: {
90
+ mode: 'allowlist' | 'blocklist'; // Allow only listed, or allow all except listed
91
+ list: string[]; // Skill IDs
92
+ };
93
+
94
+ // Tool-level overrides (more granular than skill-level)
95
+ tools: {
96
+ blocked: string[]; // Always block these tools even if skill is allowed
97
+ allowed: string[]; // Always allow these tools even if skill is blocked
98
+ };
99
+
100
+ // Risk threshold — auto-block tools above this level
101
+ maxRiskLevel: RiskLevel;
102
+
103
+ // Side-effect restrictions
104
+ blockedSideEffects: SideEffect[]; // Block any tool with these side effects
105
+
106
+ // Approval workflows
107
+ requireApproval: {
108
+ enabled: boolean;
109
+ forRiskLevels: RiskLevel[]; // Which risk levels need human approval
110
+ forSideEffects: SideEffect[]; // Which side effects need approval
111
+ approvers: string[]; // User IDs who can approve
112
+ timeoutMinutes: number; // Auto-deny after timeout
113
+ };
114
+
115
+ // Rate limits
116
+ rateLimits: {
117
+ toolCallsPerMinute: number;
118
+ toolCallsPerHour: number;
119
+ toolCallsPerDay: number;
120
+ externalActionsPerHour: number; // Actions with side effects
121
+ };
122
+
123
+ // Execution constraints
124
+ constraints: {
125
+ maxConcurrentTasks: number;
126
+ maxSessionDurationMinutes: number;
127
+ allowedWorkingHours?: { start: string; end: string; timezone: string };
128
+ allowedIPs?: string[]; // Restrict to specific networks
129
+ sandboxMode: boolean; // If true, all external actions are simulated
130
+ };
131
+
132
+ createdAt: string;
133
+ updatedAt: string;
134
+ }
135
+
136
+ // ─── Preset Permission Profiles ─────────────────────────
137
+
138
+ export const PRESET_PROFILES: Omit<AgentPermissionProfile, 'id' | 'createdAt' | 'updatedAt'>[] = [
139
+ {
140
+ name: 'Research Assistant',
141
+ description: 'Can search the web, read files, and summarize content. Cannot send messages, run code, or modify anything.',
142
+ skills: { mode: 'allowlist', list: ['research', 'summarize', 'data-read'] },
143
+ tools: { blocked: ['exec', 'write', 'edit'], allowed: ['web_search', 'web_fetch', 'read', 'memory_search', 'memory_get'] },
144
+ maxRiskLevel: 'low',
145
+ blockedSideEffects: ['sends-email', 'sends-message', 'sends-sms', 'posts-social', 'runs-code', 'modifies-files', 'deletes-data', 'controls-device', 'financial'],
146
+ requireApproval: { enabled: false, forRiskLevels: [], forSideEffects: [], approvers: [], timeoutMinutes: 30 },
147
+ rateLimits: { toolCallsPerMinute: 30, toolCallsPerHour: 500, toolCallsPerDay: 5000, externalActionsPerHour: 0 },
148
+ constraints: { maxConcurrentTasks: 3, maxSessionDurationMinutes: 480, sandboxMode: false },
149
+ },
150
+ {
151
+ name: 'Customer Support Agent',
152
+ description: 'Can read/send emails, search knowledge base, and manage tickets. Cannot run code or access files.',
153
+ skills: { mode: 'allowlist', list: ['communication', 'research', 'agenticmail'] },
154
+ tools: { blocked: ['exec', 'browser', 'write', 'edit'], allowed: ['agenticmail_send', 'agenticmail_reply', 'agenticmail_inbox', 'agenticmail_read', 'agenticmail_search', 'web_search', 'web_fetch'] },
155
+ maxRiskLevel: 'medium',
156
+ blockedSideEffects: ['runs-code', 'modifies-files', 'deletes-data', 'controls-device', 'financial', 'posts-social'],
157
+ requireApproval: { enabled: true, forRiskLevels: ['high', 'critical'], forSideEffects: ['sends-email'], approvers: [], timeoutMinutes: 60 },
158
+ rateLimits: { toolCallsPerMinute: 20, toolCallsPerHour: 300, toolCallsPerDay: 3000, externalActionsPerHour: 50 },
159
+ constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: false },
160
+ },
161
+ {
162
+ name: 'Developer Assistant',
163
+ description: 'Full development capabilities: code, git, GitHub, shell. Cannot send external messages or access smart home.',
164
+ skills: { mode: 'allowlist', list: ['development', 'github', 'coding-agent', 'research', 'data'] },
165
+ tools: { blocked: ['agenticmail_send', 'message', 'tts', 'nodes'], allowed: ['exec', 'read', 'write', 'edit', 'web_search', 'web_fetch', 'browser'] },
166
+ maxRiskLevel: 'high',
167
+ blockedSideEffects: ['sends-email', 'sends-message', 'sends-sms', 'posts-social', 'controls-device', 'financial'],
168
+ requireApproval: { enabled: true, forRiskLevels: ['critical'], forSideEffects: [], approvers: [], timeoutMinutes: 15 },
169
+ rateLimits: { toolCallsPerMinute: 60, toolCallsPerHour: 1000, toolCallsPerDay: 10000, externalActionsPerHour: 100 },
170
+ constraints: { maxConcurrentTasks: 3, maxSessionDurationMinutes: 720, sandboxMode: false },
171
+ },
172
+ {
173
+ name: 'Full Access (Owner)',
174
+ description: 'Unrestricted access to all skills and tools. Use with caution.',
175
+ skills: { mode: 'blocklist', list: [] },
176
+ tools: { blocked: [], allowed: [] },
177
+ maxRiskLevel: 'critical',
178
+ blockedSideEffects: [],
179
+ requireApproval: { enabled: false, forRiskLevels: [], forSideEffects: [], approvers: [], timeoutMinutes: 30 },
180
+ rateLimits: { toolCallsPerMinute: 120, toolCallsPerHour: 5000, toolCallsPerDay: 50000, externalActionsPerHour: 500 },
181
+ constraints: { maxConcurrentTasks: 10, maxSessionDurationMinutes: 1440, sandboxMode: false },
182
+ },
183
+ {
184
+ name: 'Sandbox (Testing)',
185
+ description: 'All tools available but in simulation mode. No real external actions are taken.',
186
+ skills: { mode: 'blocklist', list: [] },
187
+ tools: { blocked: [], allowed: [] },
188
+ maxRiskLevel: 'critical',
189
+ blockedSideEffects: [],
190
+ requireApproval: { enabled: false, forRiskLevels: [], forSideEffects: [], approvers: [], timeoutMinutes: 30 },
191
+ rateLimits: { toolCallsPerMinute: 60, toolCallsPerHour: 1000, toolCallsPerDay: 10000, externalActionsPerHour: 500 },
192
+ constraints: { maxConcurrentTasks: 5, maxSessionDurationMinutes: 480, sandboxMode: true },
193
+ },
194
+ ];
195
+
196
+ // ─── Built-in Skill Catalog ─────────────────────────────
197
+
198
+ export const BUILTIN_SKILLS: Omit<SkillDefinition, 'tools'>[] = [
199
+ // Communication
200
+ { id: 'agenticmail', name: 'AgenticMail', description: 'Full email system — send, receive, organize, search, forward, reply. Agent-to-agent messaging and task delegation.', category: 'communication', risk: 'medium', icon: '📧', source: 'builtin' },
201
+ { id: 'imsg', name: 'iMessage', description: 'Send and receive iMessages and SMS via macOS.', category: 'communication', risk: 'high', icon: '💬', source: 'builtin', requires: ['macos'] },
202
+ { id: 'wacli', name: 'WhatsApp', description: 'Send WhatsApp messages and search chat history.', category: 'communication', risk: 'high', icon: '📱', source: 'builtin' },
203
+
204
+ // Development
205
+ { id: 'github', name: 'GitHub', description: 'Manage issues, PRs, CI runs, and repositories via gh CLI.', category: 'development', risk: 'medium', icon: '🐙', source: 'builtin' },
206
+ { id: 'coding-agent', name: 'Coding Agent', description: 'Run Codex CLI, Claude Code, or other coding agents as background processes.', category: 'development', risk: 'high', icon: '💻', source: 'builtin' },
207
+
208
+ // Productivity
209
+ { id: 'gog', name: 'Google Workspace', description: 'Gmail, Calendar, Drive, Contacts, Sheets, and Docs.', category: 'productivity', risk: 'medium', icon: '📅', source: 'builtin' },
210
+ { id: 'apple-notes', name: 'Apple Notes', description: 'Create, search, edit, and manage Apple Notes.', category: 'productivity', risk: 'low', icon: '📝', source: 'builtin', requires: ['macos'] },
211
+ { id: 'apple-reminders', name: 'Apple Reminders', description: 'Manage Apple Reminders lists and items.', category: 'productivity', risk: 'low', icon: '✅', source: 'builtin', requires: ['macos'] },
212
+ { id: 'bear-notes', name: 'Bear Notes', description: 'Create, search, and manage Bear notes.', category: 'productivity', risk: 'low', icon: '🐻', source: 'builtin', requires: ['macos'] },
213
+ { id: 'obsidian', name: 'Obsidian', description: 'Work with Obsidian vaults and automate via CLI.', category: 'productivity', risk: 'low', icon: '💎', source: 'builtin' },
214
+ { id: 'things-mac', name: 'Things 3', description: 'Manage tasks and projects in Things 3.', category: 'productivity', risk: 'low', icon: '☑️', source: 'builtin', requires: ['macos'] },
215
+
216
+ // Research
217
+ { id: 'web-search', name: 'Web Search', description: 'Search the web via Brave Search API.', category: 'research', risk: 'low', icon: '🔍', source: 'builtin' },
218
+ { id: 'web-fetch', name: 'Web Fetch', description: 'Fetch and extract readable content from URLs.', category: 'research', risk: 'low', icon: '🌐', source: 'builtin' },
219
+ { id: 'summarize', name: 'Summarize', description: 'Summarize or transcribe URLs, podcasts, and files.', category: 'research', risk: 'low', icon: '📄', source: 'builtin' },
220
+ { id: 'blogwatcher', name: 'Blog Watcher', description: 'Monitor blogs and RSS/Atom feeds for updates.', category: 'research', risk: 'low', icon: '📡', source: 'builtin' },
221
+
222
+ // Media
223
+ { id: 'openai-image-gen', name: 'Image Generation', description: 'Generate images via OpenAI Images API.', category: 'media', risk: 'low', icon: '🎨', source: 'builtin' },
224
+ { id: 'nano-banana-pro', name: 'Gemini Image', description: 'Generate or edit images via Gemini 3 Pro.', category: 'media', risk: 'low', icon: '🖼️', source: 'builtin' },
225
+ { id: 'tts', name: 'Text-to-Speech', description: 'Convert text to speech audio.', category: 'media', risk: 'low', icon: '🔊', source: 'builtin' },
226
+ { id: 'openai-whisper', name: 'Whisper Transcription', description: 'Transcribe audio via OpenAI Whisper API.', category: 'media', risk: 'low', icon: '🎙️', source: 'builtin' },
227
+ { id: 'video-frames', name: 'Video Frames', description: 'Extract frames or clips from videos.', category: 'media', risk: 'low', icon: '🎬', source: 'builtin' },
228
+ { id: 'gifgrep', name: 'GIF Search', description: 'Search and download GIFs.', category: 'media', risk: 'low', icon: '🎭', source: 'builtin' },
229
+
230
+ // Automation
231
+ { id: 'browser', name: 'Browser Control', description: 'Automate web browsers — navigate, click, type, screenshot.', category: 'automation', risk: 'high', icon: '🌍', source: 'builtin' },
232
+ { id: 'exec', name: 'Shell Commands', description: 'Execute shell commands on the host machine.', category: 'automation', risk: 'critical', icon: '⚡', source: 'builtin' },
233
+ { id: 'peekaboo', name: 'macOS UI Automation', description: 'Capture and automate macOS UI with Peekaboo.', category: 'automation', risk: 'high', icon: '👁️', source: 'builtin', requires: ['macos'] },
234
+ { id: 'cron', name: 'Scheduled Tasks', description: 'Create and manage cron jobs and reminders.', category: 'automation', risk: 'medium', icon: '⏰', source: 'builtin' },
235
+
236
+ // Smart Home
237
+ { id: 'openhue', name: 'Philips Hue', description: 'Control Hue lights and scenes.', category: 'smart-home', risk: 'low', icon: '💡', source: 'builtin' },
238
+ { id: 'sonoscli', name: 'Sonos', description: 'Control Sonos speakers.', category: 'smart-home', risk: 'low', icon: '🔈', source: 'builtin' },
239
+ { id: 'blucli', name: 'BluOS', description: 'Control BluOS speakers.', category: 'smart-home', risk: 'low', icon: '🎵', source: 'builtin' },
240
+ { id: 'eightctl', name: 'Eight Sleep', description: 'Control Eight Sleep pod temperature and alarms.', category: 'smart-home', risk: 'low', icon: '🛏️', source: 'builtin' },
241
+ { id: 'camsnap', name: 'IP Cameras', description: 'Capture frames from RTSP/ONVIF cameras.', category: 'smart-home', risk: 'medium', icon: '📷', source: 'builtin' },
242
+
243
+ // Data
244
+ { id: 'files', name: 'File System', description: 'Read, write, and edit files on the host.', category: 'data', risk: 'medium', icon: '📁', source: 'builtin' },
245
+ { id: 'memory', name: 'Agent Memory', description: 'Persistent memory search and storage.', category: 'data', risk: 'low', icon: '🧠', source: 'builtin' },
246
+
247
+ // Security
248
+ { id: '1password', name: '1Password', description: 'Read and manage secrets via 1Password CLI.', category: 'security', risk: 'critical', icon: '🔐', source: 'builtin' },
249
+ { id: 'healthcheck', name: 'Security Audit', description: 'Host security hardening and risk checks.', category: 'security', risk: 'medium', icon: '🛡️', source: 'builtin' },
250
+
251
+ // Social
252
+ { id: 'twitter', name: 'Twitter/X', description: 'Post tweets, read timeline, manage social presence.', category: 'social', risk: 'high', icon: '🐦', source: 'builtin' },
253
+
254
+ // Platform
255
+ { id: 'gateway', name: 'OpenClaw Gateway', description: 'Restart, configure, and update the OpenClaw gateway.', category: 'platform', risk: 'critical', icon: '⚙️', source: 'builtin' },
256
+ { id: 'sessions', name: 'Session Management', description: 'Spawn sub-agents, list sessions, send messages between sessions.', category: 'platform', risk: 'medium', icon: '🔄', source: 'builtin' },
257
+ { id: 'nodes', name: 'Node Control', description: 'Discover and control paired devices (camera, screen, location).', category: 'platform', risk: 'high', icon: '📡', source: 'builtin' },
258
+ ];
259
+
260
+ // ─── Permission Engine ──────────────────────────────────
261
+
262
+ export class PermissionEngine {
263
+ private skills: Map<string, SkillDefinition> = new Map();
264
+ private profiles: Map<string, AgentPermissionProfile> = new Map();
265
+
266
+ constructor(skills?: SkillDefinition[]) {
267
+ if (skills) {
268
+ for (const s of skills) this.skills.set(s.id, s);
269
+ }
270
+ }
271
+
272
+ registerSkill(skill: SkillDefinition) {
273
+ this.skills.set(skill.id, skill);
274
+ }
275
+
276
+ setProfile(agentId: string, profile: AgentPermissionProfile) {
277
+ this.profiles.set(agentId, profile);
278
+ }
279
+
280
+ getProfile(agentId: string): AgentPermissionProfile | undefined {
281
+ return this.profiles.get(agentId);
282
+ }
283
+
284
+ /**
285
+ * Core permission check: Can this agent use this tool right now?
286
+ * Returns { allowed, reason, requiresApproval }
287
+ */
288
+ checkPermission(
289
+ agentId: string,
290
+ toolId: string,
291
+ context?: { timestamp?: Date; ip?: string }
292
+ ): PermissionResult {
293
+ const profile = this.profiles.get(agentId);
294
+ if (!profile) {
295
+ return { allowed: false, reason: 'No permission profile assigned', requiresApproval: false };
296
+ }
297
+
298
+ // 1. Check sandbox mode
299
+ if (profile.constraints.sandboxMode) {
300
+ return { allowed: true, reason: 'Sandbox mode — action will be simulated', requiresApproval: false, sandbox: true };
301
+ }
302
+
303
+ // 2. Check working hours
304
+ if (profile.constraints.allowedWorkingHours) {
305
+ const now = context?.timestamp || new Date();
306
+ const { start, end, timezone } = profile.constraints.allowedWorkingHours;
307
+ const hour = parseInt(new Intl.DateTimeFormat('en-US', { hour: 'numeric', hour12: false, timeZone: timezone }).format(now));
308
+ const startHour = parseInt(start.split(':')[0]);
309
+ const endHour = parseInt(end.split(':')[0]);
310
+ if (hour < startHour || hour >= endHour) {
311
+ return { allowed: false, reason: `Outside working hours (${start}-${end} ${timezone})`, requiresApproval: false };
312
+ }
313
+ }
314
+
315
+ // 3. Check IP restrictions
316
+ if (profile.constraints.allowedIPs?.length && context?.ip) {
317
+ if (!profile.constraints.allowedIPs.includes(context.ip)) {
318
+ return { allowed: false, reason: `IP ${context.ip} not in allowlist`, requiresApproval: false };
319
+ }
320
+ }
321
+
322
+ // 4. Tool-level explicit overrides (highest priority)
323
+ if (profile.tools.blocked.includes(toolId)) {
324
+ return { allowed: false, reason: `Tool "${toolId}" is explicitly blocked`, requiresApproval: false };
325
+ }
326
+ if (profile.tools.allowed.includes(toolId)) {
327
+ // Explicitly allowed — skip skill checks, but still check approval requirements
328
+ return this._checkApproval(profile, toolId);
329
+ }
330
+
331
+ // 5. Find which skill this tool belongs to
332
+ const tool = this._findTool(toolId);
333
+ if (!tool) {
334
+ // Unknown tool — block by default
335
+ return { allowed: false, reason: `Unknown tool "${toolId}"`, requiresApproval: false };
336
+ }
337
+
338
+ // 6. Skill-level check
339
+ const skillAllowed = profile.skills.mode === 'allowlist'
340
+ ? profile.skills.list.includes(tool.skillId)
341
+ : !profile.skills.list.includes(tool.skillId);
342
+
343
+ if (!skillAllowed) {
344
+ return { allowed: false, reason: `Skill "${tool.skillId}" is not permitted`, requiresApproval: false };
345
+ }
346
+
347
+ // 7. Risk level check
348
+ const riskOrder: RiskLevel[] = ['low', 'medium', 'high', 'critical'];
349
+ const toolRiskIdx = riskOrder.indexOf(tool.risk);
350
+ const maxRiskIdx = riskOrder.indexOf(profile.maxRiskLevel);
351
+ if (toolRiskIdx > maxRiskIdx) {
352
+ return { allowed: false, reason: `Tool risk "${tool.risk}" exceeds max allowed "${profile.maxRiskLevel}"`, requiresApproval: false };
353
+ }
354
+
355
+ // 8. Side-effect restrictions
356
+ for (const effect of tool.sideEffects) {
357
+ if (profile.blockedSideEffects.includes(effect)) {
358
+ return { allowed: false, reason: `Side effect "${effect}" is blocked`, requiresApproval: false };
359
+ }
360
+ }
361
+
362
+ // 9. Check if approval is required
363
+ return this._checkApproval(profile, toolId, tool);
364
+ }
365
+
366
+ private _checkApproval(profile: AgentPermissionProfile, toolId: string, tool?: ToolDefinition): PermissionResult {
367
+ if (!profile.requireApproval.enabled) {
368
+ return { allowed: true, reason: 'Permitted', requiresApproval: false };
369
+ }
370
+
371
+ if (tool) {
372
+ // Check risk-level approval
373
+ if (profile.requireApproval.forRiskLevels.includes(tool.risk)) {
374
+ return { allowed: true, reason: 'Requires human approval (risk level)', requiresApproval: true };
375
+ }
376
+ // Check side-effect approval
377
+ for (const effect of tool.sideEffects) {
378
+ if (profile.requireApproval.forSideEffects.includes(effect)) {
379
+ return { allowed: true, reason: `Requires human approval (${effect})`, requiresApproval: true };
380
+ }
381
+ }
382
+ }
383
+
384
+ return { allowed: true, reason: 'Permitted', requiresApproval: false };
385
+ }
386
+
387
+ private _findTool(toolId: string): ToolDefinition | undefined {
388
+ // Check registered skills first
389
+ for (const skill of this.skills.values()) {
390
+ const tool = skill.tools.find(t => t.id === toolId);
391
+ if (tool) return tool;
392
+ }
393
+ // Fall back to global tool index (real OpenClaw + AgenticMail tools)
394
+ try {
395
+ const { TOOL_INDEX } = require('./tool-catalog.js');
396
+ return TOOL_INDEX.get(toolId);
397
+ } catch {
398
+ return undefined;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Get the full resolved tool list for an agent — what they can actually use
404
+ */
405
+ getAvailableTools(agentId: string): { tool: ToolDefinition; status: 'allowed' | 'approval-required' | 'sandbox' }[] {
406
+ const result: { tool: ToolDefinition; status: 'allowed' | 'approval-required' | 'sandbox' }[] = [];
407
+
408
+ for (const skill of this.skills.values()) {
409
+ for (const tool of skill.tools) {
410
+ const perm = this.checkPermission(agentId, tool.id);
411
+ if (perm.allowed) {
412
+ result.push({
413
+ tool,
414
+ status: perm.sandbox ? 'sandbox' : perm.requiresApproval ? 'approval-required' : 'allowed',
415
+ });
416
+ }
417
+ }
418
+ }
419
+
420
+ return result;
421
+ }
422
+
423
+ /**
424
+ * Generate the OpenClaw tool policy config for an agent based on their profile
425
+ */
426
+ generateToolPolicy(agentId: string): {
427
+ allowedTools: string[];
428
+ blockedTools: string[];
429
+ approvalRequired: string[];
430
+ rateLimits: AgentPermissionProfile['rateLimits'];
431
+ } {
432
+ const profile = this.profiles.get(agentId);
433
+ if (!profile) return { allowedTools: [], blockedTools: [], approvalRequired: [], rateLimits: { toolCallsPerMinute: 10, toolCallsPerHour: 100, toolCallsPerDay: 1000, externalActionsPerHour: 10 } };
434
+
435
+ const allowed: string[] = [];
436
+ const blocked: string[] = [];
437
+ const approval: string[] = [];
438
+
439
+ for (const skill of this.skills.values()) {
440
+ for (const tool of skill.tools) {
441
+ const perm = this.checkPermission(agentId, tool.id);
442
+ if (perm.allowed) {
443
+ allowed.push(tool.id);
444
+ if (perm.requiresApproval) approval.push(tool.id);
445
+ } else {
446
+ blocked.push(tool.id);
447
+ }
448
+ }
449
+ }
450
+
451
+ return { allowedTools: allowed, blockedTools: blocked, approvalRequired: approval, rateLimits: profile.rateLimits };
452
+ }
453
+
454
+ getAllSkills(): SkillDefinition[] {
455
+ return Array.from(this.skills.values());
456
+ }
457
+
458
+ getSkillsByCategory(): Record<SkillCategory, SkillDefinition[]> {
459
+ const result = {} as Record<SkillCategory, SkillDefinition[]>;
460
+ for (const skill of this.skills.values()) {
461
+ if (!result[skill.category]) result[skill.category] = [];
462
+ result[skill.category].push(skill);
463
+ }
464
+ return result;
465
+ }
466
+ }
467
+
468
+ export interface PermissionResult {
469
+ allowed: boolean;
470
+ reason: string;
471
+ requiresApproval: boolean;
472
+ sandbox?: boolean;
473
+ }