agentgate 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -4
- package/package.json +3 -2
- package/public/mobile.css +178 -0
- package/public/style.css +129 -0
- package/src/index.js +232 -32
- package/src/lib/agentNotifier.js +82 -1
- package/src/lib/db.js +825 -8
- package/src/routes/agents.js +49 -8
- package/src/routes/linkedin.js +30 -10
- package/src/routes/memento.js +106 -0
- package/src/routes/queue.js +165 -6
- package/src/routes/services.js +87 -0
- package/src/routes/ui/access.js +290 -0
- package/src/routes/ui/calendar.js +65 -14
- package/src/routes/ui/fitbit.js +63 -14
- package/src/routes/ui/home.js +313 -0
- package/src/routes/ui/index.js +52 -35
- package/src/routes/ui/keys.js +561 -11
- package/src/routes/ui/linkedin.js +75 -19
- package/src/routes/ui/mastodon.js +70 -18
- package/src/routes/ui/mementos.js +363 -0
- package/src/routes/ui/messages.js +155 -18
- package/src/routes/ui/queue.js +193 -14
- package/src/routes/ui/reddit.js +63 -14
- package/src/routes/ui/services.js +46 -0
- package/src/routes/ui/shared.js +137 -7
- package/src/routes/ui/youtube.js +63 -14
- package/src/routes/webhooks.js +247 -0
- package/src/routes/ui.js +0 -2151
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import express from 'express';
|
|
|
2
2
|
import cookieParser from 'cookie-parser';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
-
import { validateApiKey, getAccountsByService, getCookieSecret, getMessagingMode } from './lib/db.js';
|
|
5
|
+
import { validateApiKey, getAccountsByService, getCookieSecret, getMessagingMode, checkServiceAccess } from './lib/db.js';
|
|
6
6
|
import { connectHsync } from './lib/hsyncManager.js';
|
|
7
7
|
import { initSocket } from './lib/socketManager.js';
|
|
8
8
|
import githubRoutes, { serviceInfo as githubInfo } from './routes/github.js';
|
|
@@ -16,7 +16,10 @@ import jiraRoutes, { serviceInfo as jiraInfo } from './routes/jira.js';
|
|
|
16
16
|
import fitbitRoutes, { serviceInfo as fitbitInfo } from './routes/fitbit.js';
|
|
17
17
|
import queueRoutes from './routes/queue.js';
|
|
18
18
|
import agentsRoutes from './routes/agents.js';
|
|
19
|
-
import
|
|
19
|
+
import mementoRoutes from './routes/memento.js';
|
|
20
|
+
import uiRoutes from './routes/ui/index.js';
|
|
21
|
+
import webhooksRoutes from './routes/webhooks.js';
|
|
22
|
+
import servicesRoutes from './routes/services.js';
|
|
20
23
|
|
|
21
24
|
// Aggregate service metadata from all routes
|
|
22
25
|
const SERVICE_REGISTRY = {
|
|
@@ -36,7 +39,15 @@ const app = express();
|
|
|
36
39
|
const PORT = process.env.PORT || 3050;
|
|
37
40
|
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
|
|
38
41
|
|
|
39
|
-
app.use(express.json({
|
|
42
|
+
app.use(express.json({
|
|
43
|
+
limit: '10mb',
|
|
44
|
+
verify: (req, res, buf) => {
|
|
45
|
+
// Capture raw body for webhook signature verification
|
|
46
|
+
if (req.originalUrl.startsWith('/webhooks')) {
|
|
47
|
+
req.rawBody = buf;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
40
51
|
app.use(express.urlencoded({ extended: true }));
|
|
41
52
|
app.use(cookieParser(getCookieSecret()));
|
|
42
53
|
app.use('/public', express.static(join(__dirname, '../public')));
|
|
@@ -54,6 +65,11 @@ async function apiKeyAuth(req, res, next) {
|
|
|
54
65
|
return res.status(401).json({ error: 'Invalid API key' });
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
if (!valid.enabled) {
|
|
69
|
+
return res.status(403).json({ error: 'Agent is disabled' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
57
73
|
req.apiKeyInfo = valid;
|
|
58
74
|
next();
|
|
59
75
|
}
|
|
@@ -66,17 +82,49 @@ function readOnlyEnforce(req, res, next) {
|
|
|
66
82
|
next();
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
|
|
86
|
+
// Service access control middleware factory
|
|
87
|
+
// Checks if the agent has access to the requested service/account
|
|
88
|
+
function serviceAccessCheck(serviceName) {
|
|
89
|
+
return (req, res, next) => {
|
|
90
|
+
// Extract accountName from the URL path (first segment after the service mount point)
|
|
91
|
+
// e.g., /api/github/monteslu/repos -> req.path = /monteslu/repos -> accountName = monteslu
|
|
92
|
+
const pathSegments = req.path.split('/').filter(Boolean);
|
|
93
|
+
const accountName = pathSegments[0];
|
|
94
|
+
if (!accountName) {
|
|
95
|
+
return next(); // No account specified, let the route handle it
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const agentName = req.apiKeyInfo?.name;
|
|
99
|
+
if (!agentName) {
|
|
100
|
+
return next(); // No agent info, let other middleware handle auth
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const access = checkServiceAccess(serviceName, accountName, agentName);
|
|
104
|
+
if (!access.allowed) {
|
|
105
|
+
return res.status(403).json({
|
|
106
|
+
error: `Agent '${agentName}' does not have access to service '${serviceName}/${accountName}'`,
|
|
107
|
+
reason: access.reason
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
next();
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// API routes - require auth, read-only, and service access check
|
|
70
115
|
// Pattern: /api/{service}/{accountName}/...
|
|
71
|
-
app.use('/api/github', apiKeyAuth, readOnlyEnforce, githubRoutes);
|
|
72
|
-
app.use('/api/bluesky', apiKeyAuth, readOnlyEnforce, blueskyRoutes);
|
|
73
|
-
app.use('/api/reddit', apiKeyAuth, readOnlyEnforce, redditRoutes);
|
|
74
|
-
app.use('/api/calendar', apiKeyAuth, readOnlyEnforce, calendarRoutes);
|
|
75
|
-
app.use('/api/mastodon', apiKeyAuth, readOnlyEnforce, mastodonRoutes);
|
|
76
|
-
app.use('/api/linkedin', apiKeyAuth, readOnlyEnforce, linkedinRoutes);
|
|
77
|
-
app.use('/api/youtube', apiKeyAuth, readOnlyEnforce, youtubeRoutes);
|
|
78
|
-
app.use('/api/jira', apiKeyAuth, readOnlyEnforce, jiraRoutes);
|
|
79
|
-
app.use('/api/fitbit', apiKeyAuth, readOnlyEnforce, fitbitRoutes);
|
|
116
|
+
app.use('/api/github', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('github'), githubRoutes);
|
|
117
|
+
app.use('/api/bluesky', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('bluesky'), blueskyRoutes);
|
|
118
|
+
app.use('/api/reddit', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('reddit'), redditRoutes);
|
|
119
|
+
app.use('/api/calendar', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('calendar'), calendarRoutes);
|
|
120
|
+
app.use('/api/mastodon', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('mastodon'), mastodonRoutes);
|
|
121
|
+
app.use('/api/linkedin', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('linkedin'), linkedinRoutes);
|
|
122
|
+
app.use('/api/youtube', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('youtube'), youtubeRoutes);
|
|
123
|
+
app.use('/api/jira', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('jira'), jiraRoutes);
|
|
124
|
+
app.use('/api/fitbit', apiKeyAuth, readOnlyEnforce, serviceAccessCheck('fitbit'), fitbitRoutes);
|
|
125
|
+
|
|
126
|
+
// Service access management - admin API (requires auth)
|
|
127
|
+
app.use('/api/services', apiKeyAuth, servicesRoutes);
|
|
80
128
|
|
|
81
129
|
// Queue routes - require auth but allow POST for submitting write requests
|
|
82
130
|
// Pattern: /api/queue/{service}/{accountName}/submit
|
|
@@ -89,35 +137,59 @@ app.use('/api/agents', apiKeyAuth, (req, res, next) => {
|
|
|
89
137
|
next();
|
|
90
138
|
}, agentsRoutes);
|
|
91
139
|
|
|
140
|
+
// Memento routes - require auth, allow POST for creating mementos
|
|
141
|
+
app.use('/api/agents/memento', apiKeyAuth, (req, res, next) => {
|
|
142
|
+
req.apiKeyName = req.apiKeyInfo.name;
|
|
143
|
+
next();
|
|
144
|
+
}, mementoRoutes);
|
|
145
|
+
|
|
92
146
|
// UI routes - no API key needed (local admin access)
|
|
93
147
|
app.use('/ui', uiRoutes);
|
|
94
148
|
|
|
149
|
+
// Webhook routes - no API key needed (uses signature verification instead)
|
|
150
|
+
app.use('/webhooks', webhooksRoutes);
|
|
151
|
+
|
|
95
152
|
// Agent readme endpoint - requires auth
|
|
96
153
|
app.get('/api/readme', apiKeyAuth, (req, res) => {
|
|
97
154
|
const accountsByService = getAccountsByService();
|
|
155
|
+
const agentName = req.apiKeyInfo?.name;
|
|
98
156
|
|
|
99
|
-
// Build services object from registry
|
|
157
|
+
// Build services object from registry, filtering by agent access
|
|
100
158
|
const services = {};
|
|
101
159
|
for (const [key, info] of Object.entries(SERVICE_REGISTRY)) {
|
|
102
160
|
const dbKey = info.dbKey || key;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
161
|
+
const allAccounts = accountsByService[dbKey] || [];
|
|
162
|
+
|
|
163
|
+
// Filter accounts based on agent access
|
|
164
|
+
const accessibleAccounts = allAccounts.filter(accountName => {
|
|
165
|
+
const access = checkServiceAccess(key, accountName, agentName);
|
|
166
|
+
return access.allowed;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Only include service if agent has access to at least one account
|
|
170
|
+
if (accessibleAccounts.length > 0) {
|
|
171
|
+
services[key] = {
|
|
172
|
+
accounts: accessibleAccounts,
|
|
173
|
+
authType: info.authType,
|
|
174
|
+
description: info.description
|
|
175
|
+
};
|
|
176
|
+
}
|
|
108
177
|
}
|
|
109
178
|
|
|
110
|
-
// Build endpoints object from registry
|
|
179
|
+
// Build endpoints object from registry (only for accessible services)
|
|
111
180
|
const endpoints = {};
|
|
112
181
|
for (const [key, info] of Object.entries(SERVICE_REGISTRY)) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
182
|
+
// Only include endpoint if agent has access to this service
|
|
183
|
+
if (services[key]) {
|
|
184
|
+
endpoints[key] = {
|
|
185
|
+
base: `/api/${key}/{accountName}`,
|
|
186
|
+
description: info.description,
|
|
187
|
+
docs: info.docs,
|
|
188
|
+
examples: info.examples
|
|
189
|
+
};
|
|
190
|
+
if (info.writeGuidelines) {
|
|
191
|
+
endpoints[key].writeGuidelines = info.writeGuidelines;
|
|
192
|
+
}
|
|
121
193
|
}
|
|
122
194
|
}
|
|
123
195
|
|
|
@@ -194,12 +266,15 @@ app.get('/api/readme', apiKeyAuth, (req, res) => {
|
|
|
194
266
|
description: 'Withdraw your own pending submission (requires agent_withdraw_enabled setting)',
|
|
195
267
|
method: 'DELETE',
|
|
196
268
|
path: '/api/queue/{service}/{accountName}/status/{id}',
|
|
269
|
+
body: {
|
|
270
|
+
reason: 'Optional: Explain why you are withdrawing this request'
|
|
271
|
+
},
|
|
197
272
|
constraints: [
|
|
198
273
|
'Only the submitting agent can withdraw their own items',
|
|
199
274
|
'Only works for "pending" status - cannot withdraw approved/completed/etc',
|
|
200
275
|
'Requires admin to enable agent_withdraw_enabled setting'
|
|
201
276
|
],
|
|
202
|
-
response: '{ success: true, message: "Queue entry withdrawn", id }'
|
|
277
|
+
response: '{ success: true, message: "Queue entry withdrawn", id, reason }'
|
|
203
278
|
},
|
|
204
279
|
statuses: {
|
|
205
280
|
pending: 'Waiting for human approval',
|
|
@@ -327,10 +402,17 @@ app.get('/api/readme', apiKeyAuth, (req, res) => {
|
|
|
327
402
|
body: { message: 'Your broadcast message' },
|
|
328
403
|
response: '{ delivered: ["Agent1", "Agent2"], failed: [{ name: "Agent3", error: "HTTP 500" }], total: 3 }',
|
|
329
404
|
notes: [
|
|
330
|
-
'Broadcasts
|
|
405
|
+
'Broadcasts are stored in the database and appear in message history',
|
|
331
406
|
'Sender is automatically excluded from recipients',
|
|
332
|
-
'Requires messaging mode to be "supervised" or "open" (not "off")'
|
|
407
|
+
'Requires messaging mode to be "supervised" or "open" (not "off")',
|
|
408
|
+
'View broadcast history in Admin UI under Messages → Broadcast'
|
|
333
409
|
]
|
|
410
|
+
},
|
|
411
|
+
getBroadcasts: {
|
|
412
|
+
method: 'GET',
|
|
413
|
+
path: '/api/agents/messages',
|
|
414
|
+
description: 'Broadcasts appear in your regular message history with from="[BROADCAST]"',
|
|
415
|
+
note: 'No separate endpoint - broadcasts are included with regular messages'
|
|
334
416
|
}
|
|
335
417
|
},
|
|
336
418
|
modes: {
|
|
@@ -344,7 +426,124 @@ app.get('/api/readme', apiKeyAuth, (req, res) => {
|
|
|
344
426
|
'Maximum message length is 10KB'
|
|
345
427
|
]
|
|
346
428
|
};
|
|
347
|
-
})()
|
|
429
|
+
})(),
|
|
430
|
+
memento: {
|
|
431
|
+
description: 'Durable memory storage for agents. Store and retrieve memory snapshots tagged with keywords.',
|
|
432
|
+
design: {
|
|
433
|
+
appendOnly: 'Mementos are immutable once stored. New memories can be added but not edited.',
|
|
434
|
+
keywordTagging: 'Each memento has 1-10 keywords. Keywords are normalized (lowercase) and stemmed (Porter stemmer).',
|
|
435
|
+
twoStepRetrieval: 'Search returns metadata only. Fetch specific IDs to get full content. This prevents context bloat.',
|
|
436
|
+
tokenBudget: 'Recommended ~1.5-2K tokens per memento. Hard cap: 12KB characters.'
|
|
437
|
+
},
|
|
438
|
+
endpoints: {
|
|
439
|
+
store: {
|
|
440
|
+
method: 'POST',
|
|
441
|
+
path: '/api/agents/memento',
|
|
442
|
+
body: {
|
|
443
|
+
content: 'Your memory content (required)',
|
|
444
|
+
keywords: ['keyword1', 'keyword2', '...'],
|
|
445
|
+
model: 'Model at time of storage (optional)',
|
|
446
|
+
role: 'Agent role/tier (optional)'
|
|
447
|
+
},
|
|
448
|
+
response: '{ id, agent_id, keywords, created_at }'
|
|
449
|
+
},
|
|
450
|
+
listKeywords: {
|
|
451
|
+
method: 'GET',
|
|
452
|
+
path: '/api/agents/memento/keywords',
|
|
453
|
+
description: 'List all keywords you have used (returned in stemmed form, e.g., "games" → "game")',
|
|
454
|
+
response: '{ keywords: [{ keyword, count }, ...] }'
|
|
455
|
+
},
|
|
456
|
+
search: {
|
|
457
|
+
method: 'GET',
|
|
458
|
+
path: '/api/agents/memento/search?keywords=game,project&limit=10',
|
|
459
|
+
description: 'Search mementos by keyword. Returns metadata only (preview, not full content).',
|
|
460
|
+
response: '{ matches: [{ id, keywords, created_at, preview, match_count }, ...] }'
|
|
461
|
+
},
|
|
462
|
+
recent: {
|
|
463
|
+
method: 'GET',
|
|
464
|
+
path: '/api/agents/memento/recent?limit=5',
|
|
465
|
+
description: 'Get most recent mementos (metadata only)',
|
|
466
|
+
response: '{ mementos: [{ id, keywords, created_at, preview }, ...] }'
|
|
467
|
+
},
|
|
468
|
+
fetch: {
|
|
469
|
+
method: 'GET',
|
|
470
|
+
path: '/api/agents/memento/42,38,15',
|
|
471
|
+
description: 'Fetch full content by IDs (comma-separated, max 20)',
|
|
472
|
+
response: '{ mementos: [{ id, agent_id, model, role, keywords, content, created_at }, ...] }'
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
retrievalHierarchy: [
|
|
476
|
+
'1. Check current context first — already in conversation?',
|
|
477
|
+
'2. Query Memento — if not in context, search by keyword',
|
|
478
|
+
'3. Web search — if no memento, fall back to internet'
|
|
479
|
+
],
|
|
480
|
+
notes: [
|
|
481
|
+
'Each agent sees only their own mementos',
|
|
482
|
+
'Keywords are stemmed: "games" matches "game", "running" matches "run"',
|
|
483
|
+
'Maximum 10 keywords per memento',
|
|
484
|
+
'Maximum 12KB content per memento'
|
|
485
|
+
]
|
|
486
|
+
},
|
|
487
|
+
serviceAccess: {
|
|
488
|
+
description: 'Service-level access control. Restrict which agents can access specific services.',
|
|
489
|
+
accessModes: {
|
|
490
|
+
all: 'All agents can access (default)',
|
|
491
|
+
allowlist: 'Only listed agents can access',
|
|
492
|
+
denylist: 'All agents EXCEPT listed ones can access'
|
|
493
|
+
},
|
|
494
|
+
endpoints: {
|
|
495
|
+
list: {
|
|
496
|
+
method: 'GET',
|
|
497
|
+
path: '/api/services',
|
|
498
|
+
description: 'List all services with their access configuration',
|
|
499
|
+
response: '{ services: [{ service, account_name, access_mode, agent_count }, ...] }'
|
|
500
|
+
},
|
|
501
|
+
getAccess: {
|
|
502
|
+
method: 'GET',
|
|
503
|
+
path: '/api/services/:service/:account/access',
|
|
504
|
+
description: 'Get access config for a specific service/account',
|
|
505
|
+
response: '{ service, account_name, access_mode, agents: [{ name, allowed }, ...] }'
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
errorResponse: {
|
|
509
|
+
status: 403,
|
|
510
|
+
body: '{ error: "Agent \'X\' does not have access to service \'Y/Z\'", reason: "not_in_allowlist" }'
|
|
511
|
+
},
|
|
512
|
+
notes: [
|
|
513
|
+
'Access checks apply to all service API calls',
|
|
514
|
+
'Default mode is "all" (backwards compatible)',
|
|
515
|
+
'Agent names are case-insensitive',
|
|
516
|
+
'Admin UI shows visual indicators for restricted services'
|
|
517
|
+
]
|
|
518
|
+
},
|
|
519
|
+
bypassAuth: {
|
|
520
|
+
description: 'Trusted agents can bypass the write queue entirely, executing write operations immediately without human approval.',
|
|
521
|
+
warning: 'Use with extreme caution. Only enable for agents you completely trust with unsupervised write access.',
|
|
522
|
+
setup: {
|
|
523
|
+
description: 'Configure in Admin UI under API Keys → Configure → Auth Bypass',
|
|
524
|
+
note: 'This is a per-agent setting managed by the admin'
|
|
525
|
+
},
|
|
526
|
+
behavior: {
|
|
527
|
+
enabled: [
|
|
528
|
+
'All write operations (POST/PUT/DELETE) execute immediately',
|
|
529
|
+
'No queue entries are created',
|
|
530
|
+
'The agent is effectively operating unsupervised',
|
|
531
|
+
'Reads work the same as before'
|
|
532
|
+
],
|
|
533
|
+
disabled: 'Default behavior - writes are queued for human approval'
|
|
534
|
+
},
|
|
535
|
+
checkStatus: {
|
|
536
|
+
method: 'GET',
|
|
537
|
+
path: '/api/agents/status',
|
|
538
|
+
description: 'Check if your agent has bypass_auth enabled',
|
|
539
|
+
response: '{ mode, enabled, unread_count, bypass_auth: true|false }'
|
|
540
|
+
},
|
|
541
|
+
notes: [
|
|
542
|
+
'Bypass applies to all services the agent has access to',
|
|
543
|
+
'Useful for automation agents that need to perform routine operations',
|
|
544
|
+
'Admin can revoke bypass at any time'
|
|
545
|
+
]
|
|
546
|
+
}
|
|
348
547
|
});
|
|
349
548
|
});
|
|
350
549
|
|
|
@@ -504,3 +703,4 @@ server.on('error', (err) => {
|
|
|
504
703
|
}
|
|
505
704
|
process.exit(1);
|
|
506
705
|
});
|
|
706
|
+
|
package/src/lib/agentNotifier.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Agent notification delivery - sends webhooks to agent gateways
|
|
2
|
-
import { getApiKeyByName, updateQueueNotification } from './db.js';
|
|
2
|
+
import { getApiKeyByName, updateQueueNotification, getQueueWarnings } from './db.js';
|
|
3
3
|
|
|
4
4
|
// Send a notification to an agent's webhook
|
|
5
5
|
export async function notifyAgent(agentName, payload) {
|
|
@@ -83,9 +83,90 @@ export async function notifyAgentQueueStatus(entry) {
|
|
|
83
83
|
updateQueueNotification(entry.id, false, result.error);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Also notify any agents who warned on this item
|
|
87
|
+
await notifyWarningAgentsOnResolution(entry);
|
|
88
|
+
|
|
86
89
|
return result;
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
// Notify all agents who warned on a queue item when it's resolved
|
|
93
|
+
async function notifyWarningAgentsOnResolution(entry) {
|
|
94
|
+
const warnings = getQueueWarnings(entry.id);
|
|
95
|
+
if (!warnings || warnings.length === 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get unique warning agent IDs (excluding the submitter - they already get notified)
|
|
100
|
+
const warningAgents = [...new Set(warnings.map(w => w.agent_id))]
|
|
101
|
+
.filter(agentId => agentId !== entry.submitted_by);
|
|
102
|
+
|
|
103
|
+
if (warningAgents.length === 0) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const statusEmoji = {
|
|
108
|
+
completed: '✅',
|
|
109
|
+
failed: '❌',
|
|
110
|
+
rejected: '🚫',
|
|
111
|
+
approved: '✅'
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
for (const agentId of warningAgents) {
|
|
115
|
+
const payload = {
|
|
116
|
+
type: 'queue_warning_resolved',
|
|
117
|
+
entry: {
|
|
118
|
+
id: entry.id,
|
|
119
|
+
service: entry.service,
|
|
120
|
+
account_name: entry.account_name,
|
|
121
|
+
status: entry.status,
|
|
122
|
+
comment: entry.comment,
|
|
123
|
+
rejection_reason: entry.rejection_reason,
|
|
124
|
+
submitted_by: entry.submitted_by
|
|
125
|
+
},
|
|
126
|
+
// Human-readable for Clawdbot-style gateways
|
|
127
|
+
text: `${statusEmoji[entry.status] || '📋'} [agentgate] Queue #${entry.id.substring(0, 8)} you warned on was ${entry.status}\n→ ${entry.service}/${entry.account_name}\nSubmitted by: ${entry.submitted_by}${entry.rejection_reason ? `\nReason: ${entry.rejection_reason}` : ''}`,
|
|
128
|
+
mode: 'now'
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Fire and forget - don't block on warning agent notifications
|
|
132
|
+
notifyAgent(agentId, payload).catch(err => {
|
|
133
|
+
console.error(`[agentNotifier] Failed to notify warning agent ${agentId}:`, err.message);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Notify agent about a warning on their queue submission
|
|
139
|
+
export async function notifyAgentQueueWarning(entry, warningAgent, warningMessage) {
|
|
140
|
+
const agentName = entry.submitted_by;
|
|
141
|
+
if (!agentName) {
|
|
142
|
+
return { success: false, error: 'No submitter on queue entry' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const agent = getApiKeyByName(agentName);
|
|
146
|
+
if (!agent?.webhook_url) {
|
|
147
|
+
return { success: false, error: 'No webhook configured' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const payload = {
|
|
151
|
+
type: 'queue_warning',
|
|
152
|
+
entry: {
|
|
153
|
+
id: entry.id,
|
|
154
|
+
service: entry.service,
|
|
155
|
+
account_name: entry.account_name,
|
|
156
|
+
status: entry.status,
|
|
157
|
+
comment: entry.comment
|
|
158
|
+
},
|
|
159
|
+
warning: {
|
|
160
|
+
from: warningAgent,
|
|
161
|
+
message: warningMessage
|
|
162
|
+
},
|
|
163
|
+
text: `⚠️ [agentgate] Warning on Queue #${entry.id.substring(0, 8)}\n→ ${entry.service}/${entry.account_name}\nFrom: ${warningAgent}\n"${warningMessage.substring(0, 200)}"`,
|
|
164
|
+
mode: 'now'
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return await notifyAgent(agentName, payload);
|
|
168
|
+
}
|
|
169
|
+
|
|
89
170
|
// Notify agent about a new message (delivered to recipient)
|
|
90
171
|
export async function notifyAgentMessage(message) {
|
|
91
172
|
const agentName = message.to_agent;
|