@0xmaxma/claude-gateway 1.1.9 → 1.2.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.
Files changed (67) hide show
  1. package/README.md +61 -1
  2. package/dist/agent/builtin-commands.d.ts +10 -0
  3. package/dist/agent/builtin-commands.d.ts.map +1 -0
  4. package/dist/agent/builtin-commands.js +38 -0
  5. package/dist/agent/builtin-commands.js.map +1 -0
  6. package/dist/agent/runner.d.ts +7 -5
  7. package/dist/agent/runner.d.ts.map +1 -1
  8. package/dist/agent/runner.js +56 -30
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/agent/workspace-loader.d.ts.map +1 -1
  11. package/dist/agent/workspace-loader.js +9 -4
  12. package/dist/agent/workspace-loader.js.map +1 -1
  13. package/dist/api/apps-router.d.ts +7 -0
  14. package/dist/api/apps-router.d.ts.map +1 -0
  15. package/dist/api/apps-router.js +242 -0
  16. package/dist/api/apps-router.js.map +1 -0
  17. package/dist/api/gateway-router.d.ts +17 -1
  18. package/dist/api/gateway-router.d.ts.map +1 -1
  19. package/dist/api/gateway-router.js +171 -2
  20. package/dist/api/gateway-router.js.map +1 -1
  21. package/dist/api/router.d.ts.map +1 -1
  22. package/dist/api/router.js +385 -4
  23. package/dist/api/router.js.map +1 -1
  24. package/dist/apps/agent-manager.d.ts +76 -0
  25. package/dist/apps/agent-manager.d.ts.map +1 -0
  26. package/dist/apps/agent-manager.js +311 -0
  27. package/dist/apps/agent-manager.js.map +1 -0
  28. package/dist/apps/compose-generator.d.ts +108 -0
  29. package/dist/apps/compose-generator.d.ts.map +1 -0
  30. package/dist/apps/compose-generator.js +687 -0
  31. package/dist/apps/compose-generator.js.map +1 -0
  32. package/dist/apps/installer.d.ts +101 -0
  33. package/dist/apps/installer.d.ts.map +1 -0
  34. package/dist/apps/installer.js +898 -0
  35. package/dist/apps/installer.js.map +1 -0
  36. package/dist/apps/registry-client.d.ts +37 -0
  37. package/dist/apps/registry-client.d.ts.map +1 -0
  38. package/dist/apps/registry-client.js +106 -0
  39. package/dist/apps/registry-client.js.map +1 -0
  40. package/dist/apps/registry.d.ts +54 -0
  41. package/dist/apps/registry.d.ts.map +1 -0
  42. package/dist/apps/registry.js +182 -0
  43. package/dist/apps/registry.js.map +1 -0
  44. package/dist/apps/socket-server.d.ts +46 -0
  45. package/dist/apps/socket-server.d.ts.map +1 -0
  46. package/dist/apps/socket-server.js +297 -0
  47. package/dist/apps/socket-server.js.map +1 -0
  48. package/dist/config/watcher.d.ts +1 -0
  49. package/dist/config/watcher.d.ts.map +1 -1
  50. package/dist/config/watcher.js +55 -2
  51. package/dist/config/watcher.js.map +1 -1
  52. package/dist/index.js +170 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/session/process.d.ts.map +1 -1
  55. package/dist/session/process.js +47 -6
  56. package/dist/session/process.js.map +1 -1
  57. package/dist/types.d.ts +6 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/mcp/server.ts +2 -0
  60. package/mcp/tools/apps/client.ts +78 -0
  61. package/mcp/tools/apps/module.ts +211 -0
  62. package/mcp/tools/apps/skills/app-status/SKILL.md +48 -0
  63. package/mcp/tools/apps/skills/create-app-yaml/SKILL.md +94 -0
  64. package/mcp/tools/apps/skills/install-app/SKILL.md +102 -0
  65. package/mcp/tools/apps/skills/list-apps/SKILL.md +34 -0
  66. package/mcp/tools/telegram/receiver-server.ts +36 -2
  67. package/package.json +1 -1
@@ -51,6 +51,11 @@ const MAX_MESSAGE_LENGTH = 10000;
51
51
  const DEFAULT_TIMEOUT_MS = 60000;
52
52
  const AGENT_ID_RE = /^[a-z][a-z0-9_-]{1,31}$/;
53
53
  const SAFE_FILENAME_RE = /^[a-zA-Z0-9._\-() ]+$/;
54
+ function maskToken(token) {
55
+ if (token.length <= 12)
56
+ return '•'.repeat(token.length);
57
+ return token.slice(0, 8) + '•••••' + token.slice(-4);
58
+ }
54
59
  /** Detect MIME type from file magic bytes (first 12 bytes). */
55
60
  function detectMimeFromMagic(header) {
56
61
  if (header[0] === 0xFF && header[1] === 0xD8 && header[2] === 0xFF)
@@ -323,8 +328,8 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
323
328
  const timeoutMs = typeof timeout_ms === 'number' && timeout_ms > 0 && timeout_ms <= 600000
324
329
  ? timeout_ms
325
330
  : DEFAULT_TIMEOUT_MS;
326
- // Slash command dispatch — runs instead of sending to Claude
327
- if (message.trim().startsWith('/')) {
331
+ // Built-in command dispatch — only intercept known commands, let everything else reach Claude
332
+ if (runner_1.AgentRunner.isApiBuiltinCommand(message.trim())) {
328
333
  try {
329
334
  const result = await runner.executeApiCommand(sessionId, chatIdStr, message.trim());
330
335
  res.json({ command: message.trim(), session_id: sessionId, result });
@@ -455,6 +460,11 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
455
460
  model: cfg.claude?.model ?? null,
456
461
  allow_tools: cfg.allow_tools ?? false,
457
462
  avatarUrl: cfg.avatar ? `/api/v1/agents/${id}/avatar` : null,
463
+ telegram_connected: !!cfg.telegram?.botToken,
464
+ discord_connected: !!cfg.discord?.botToken,
465
+ telegram_token_preview: cfg.telegram?.botToken ? maskToken(cfg.telegram.botToken) : null,
466
+ discord_token_preview: cfg.discord?.botToken ? maskToken(cfg.discord.botToken) : null,
467
+ telegram_dm_policy: cfg.telegram?.botToken ? readTelegramAccess(id).dmPolicy : null,
458
468
  }));
459
469
  res.json({ agents });
460
470
  });
@@ -544,6 +554,19 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
544
554
  }
545
555
  return;
546
556
  }
557
+ // Update in-memory agentConfigs immediately so GET /api/v1/agents returns the new agent
558
+ // without waiting for the file watcher (~500ms debounce).
559
+ agentConfigs.set(id, {
560
+ id,
561
+ description: description.trim(),
562
+ workspace: workspaceAbs,
563
+ env: path.join(workspaceAbs, '.env'),
564
+ claude: {
565
+ model: typeof model === 'string' && model.trim() ? model.trim() : 'claude-sonnet-4-6',
566
+ dangerouslySkipPermissions: false,
567
+ extraFlags: [],
568
+ },
569
+ });
547
570
  // Config written successfully — now create workspace directory and stub files.
548
571
  const stubFiles = {
549
572
  'AGENTS.md': `# Agent: ${id}\n\n${description.trim()}\n`,
@@ -575,6 +598,40 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
575
598
  ? path.join(path.dirname(configPath), 'agents')
576
599
  : path.join(os.homedir(), '.claude-gateway', 'agents');
577
600
  }
601
+ function getTelegramStateDir(agentId) {
602
+ const agentsBase = getAgentsBaseDir();
603
+ const cfg = agentConfigs.get(agentId);
604
+ const workspace = cfg?.workspace
605
+ ? (cfg.workspace.startsWith('~') ? path.join(os.homedir(), cfg.workspace.slice(1)) : cfg.workspace)
606
+ : path.join(agentsBase, agentId, 'workspace');
607
+ return path.join(workspace, '.telegram-state');
608
+ }
609
+ function readTelegramAccess(agentId) {
610
+ const accessFile = path.join(getTelegramStateDir(agentId), 'access.json');
611
+ try {
612
+ const raw = fs.readFileSync(accessFile, 'utf8');
613
+ const parsed = JSON.parse(raw);
614
+ return {
615
+ dmPolicy: parsed.dmPolicy ?? 'pairing',
616
+ allowFrom: parsed.allowFrom ?? [],
617
+ groups: parsed.groups ?? {},
618
+ pending: parsed.pending ?? {},
619
+ };
620
+ }
621
+ catch {
622
+ return { dmPolicy: 'pairing', allowFrom: [], groups: {}, pending: {} };
623
+ }
624
+ }
625
+ function writeTelegramAccess(agentId, access) {
626
+ const stateDir = getTelegramStateDir(agentId);
627
+ try {
628
+ fs.mkdirSync(stateDir, { recursive: true });
629
+ fs.writeFileSync(path.join(stateDir, 'access.json'), JSON.stringify(access, null, 2));
630
+ }
631
+ catch (err) {
632
+ throw new Error(`Failed to write Telegram access config: ${err.message}`);
633
+ }
634
+ }
578
635
  /**
579
636
  * POST /api/v1/agents/wizard/start
580
637
  * Start wizard: call Claude to generate workspace files, return wizardId + preview.
@@ -799,6 +856,17 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
799
856
  });
800
857
  return;
801
858
  }
859
+ // Update in-memory agentConfigs immediately so GET /api/v1/agents returns the new agent
860
+ // without waiting for the file watcher (~500ms debounce).
861
+ agentConfigs.set(agentId, {
862
+ id: agentId,
863
+ description: wizard.prompt.slice(0, 200).trim(),
864
+ workspace: workspaceDirAbs,
865
+ env: path.join(workspaceDirAbs, '.env'),
866
+ claude: { model: defaultModel, dangerouslySkipPermissions: false, extraFlags: [] },
867
+ ...(wizard.signatureEmoji ? { signatureEmoji: wizard.signatureEmoji } : {}),
868
+ ...(avatarFilename ? { avatar: avatarFilename } : {}),
869
+ });
802
870
  wizard_state_1.wizardStore.update(wizardId, { step: 'confirmed' });
803
871
  const avatarUrl = avatarFilename ? `/api/v1/agents/${agentId}/avatar` : null;
804
872
  res.json({
@@ -951,6 +1019,12 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
951
1019
  });
952
1020
  }
953
1021
  catch { /* non-fatal */ }
1022
+ // Hot-start the receiver so the agent responds immediately without a gateway restart
1023
+ const runner = agentRunners.get(wizard.agentId);
1024
+ if (runner) {
1025
+ runner.updateAgentConfig({ ...runner.getAgentConfig(), telegram: { botToken: wizard.botToken } });
1026
+ runner.startTelegramReceiver();
1027
+ }
954
1028
  wizard_state_1.wizardStore.delete(wizardId);
955
1029
  res.json({ success: true, agentId: wizard.agentId });
956
1030
  });
@@ -1000,7 +1074,7 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
1000
1074
  return;
1001
1075
  }
1002
1076
  const body = req.body;
1003
- const { description, model, allow_tools } = body;
1077
+ const { description, model, allow_tools, telegram_bot_token, discord_bot_token } = body;
1004
1078
  if (description !== undefined && (typeof description !== 'string' || !description.trim())) {
1005
1079
  res.status(400).json({ error: 'description must be a non-empty string' });
1006
1080
  return;
@@ -1013,6 +1087,14 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
1013
1087
  res.status(400).json({ error: 'allow_tools must be a boolean' });
1014
1088
  return;
1015
1089
  }
1090
+ if (telegram_bot_token !== undefined && telegram_bot_token !== null && typeof telegram_bot_token !== 'string') {
1091
+ res.status(400).json({ error: 'telegram_bot_token must be a string or null' });
1092
+ return;
1093
+ }
1094
+ if (discord_bot_token !== undefined && discord_bot_token !== null && typeof discord_bot_token !== 'string') {
1095
+ res.status(400).json({ error: 'discord_bot_token must be a string or null' });
1096
+ return;
1097
+ }
1016
1098
  try {
1017
1099
  await writeAgentsToConfig(configPath, (agents) => {
1018
1100
  const agent = agents.find((a) => a.id === agentId);
@@ -1027,6 +1109,23 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
1027
1109
  }
1028
1110
  if (allow_tools !== undefined)
1029
1111
  agent.allow_tools = allow_tools;
1112
+ if (telegram_bot_token !== undefined) {
1113
+ if (telegram_bot_token === null || telegram_bot_token === '') {
1114
+ delete agent.telegram;
1115
+ }
1116
+ else {
1117
+ agent.telegram = { botToken: telegram_bot_token.trim() };
1118
+ }
1119
+ }
1120
+ if (discord_bot_token !== undefined) {
1121
+ if (discord_bot_token === null || discord_bot_token === '') {
1122
+ delete agent.discord;
1123
+ }
1124
+ else {
1125
+ const existing = agent.discord;
1126
+ agent.discord = { ...(existing ?? {}), botToken: discord_bot_token.trim() };
1127
+ }
1128
+ }
1030
1129
  });
1031
1130
  }
1032
1131
  catch (err) {
@@ -1041,7 +1140,289 @@ function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models
1041
1140
  cfg.claude.model = model.trim();
1042
1141
  if (allow_tools !== undefined)
1043
1142
  cfg.allow_tools = allow_tools;
1044
- res.json({ agent: { id: agentId, description: cfg.description, model: cfg.claude?.model, allow_tools: cfg.allow_tools ?? false } });
1143
+ if (telegram_bot_token !== undefined) {
1144
+ const token = typeof telegram_bot_token === 'string' ? telegram_bot_token.trim() : null;
1145
+ if (token) {
1146
+ cfg.telegram = { botToken: token };
1147
+ // Hot-start receiver if not already running
1148
+ const runner = agentRunners.get(agentId);
1149
+ if (runner) {
1150
+ runner.updateAgentConfig(cfg);
1151
+ runner.startTelegramReceiver();
1152
+ }
1153
+ }
1154
+ else {
1155
+ delete cfg.telegram;
1156
+ agentRunners.get(agentId)?.stopTelegramReceiver();
1157
+ }
1158
+ }
1159
+ if (discord_bot_token !== undefined) {
1160
+ const token = typeof discord_bot_token === 'string' ? discord_bot_token.trim() : null;
1161
+ if (token) {
1162
+ cfg.discord = { ...(cfg.discord ?? {}), botToken: token };
1163
+ // Hot-start receiver if not already running
1164
+ const runner = agentRunners.get(agentId);
1165
+ if (runner) {
1166
+ runner.updateAgentConfig(cfg);
1167
+ runner.startDiscordReceiver();
1168
+ }
1169
+ }
1170
+ else {
1171
+ delete cfg.discord;
1172
+ agentRunners.get(agentId)?.stopDiscordReceiver();
1173
+ }
1174
+ }
1175
+ res.json({
1176
+ agent: {
1177
+ id: agentId,
1178
+ description: cfg.description,
1179
+ model: cfg.claude?.model,
1180
+ allow_tools: cfg.allow_tools ?? false,
1181
+ telegram_connected: !!cfg.telegram?.botToken,
1182
+ discord_connected: !!cfg.discord?.botToken,
1183
+ telegram_token_preview: cfg.telegram?.botToken ? maskToken(cfg.telegram.botToken) : null,
1184
+ discord_token_preview: cfg.discord?.botToken ? maskToken(cfg.discord.botToken) : null,
1185
+ telegram_dm_policy: cfg.telegram?.botToken ? readTelegramAccess(agentId).dmPolicy : null,
1186
+ },
1187
+ });
1188
+ });
1189
+ /**
1190
+ * GET /api/v1/agents/:agentId/telegram/pending
1191
+ * List pending Telegram pairing requests (non-expired).
1192
+ */
1193
+ router.get('/v1/agents/:agentId/telegram/pending', auth, (req, res) => {
1194
+ const { agentId } = req.params;
1195
+ const apiKey = req.apiKey;
1196
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1197
+ res.status(403).json({ error: 'Admin key required' });
1198
+ return;
1199
+ }
1200
+ if (!agentConfigs.has(agentId)) {
1201
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1202
+ return;
1203
+ }
1204
+ const access = readTelegramAccess(agentId);
1205
+ const now = Date.now();
1206
+ const expired = Object.keys(access.pending).filter((code) => access.pending[code].expiresAt <= now);
1207
+ if (expired.length > 0) {
1208
+ expired.forEach((code) => { delete access.pending[code]; });
1209
+ try {
1210
+ writeTelegramAccess(agentId, access);
1211
+ }
1212
+ catch { /* non-fatal cleanup */ }
1213
+ }
1214
+ const pending = Object.entries(access.pending)
1215
+ .map(([code, p]) => ({ code, senderId: p.senderId, chatId: p.chatId, createdAt: p.createdAt, expiresAt: p.expiresAt }));
1216
+ res.json({ pending });
1217
+ });
1218
+ /**
1219
+ * POST /api/v1/agents/:agentId/telegram/approve
1220
+ * Approve a pending Telegram pairing by code.
1221
+ */
1222
+ router.post('/v1/agents/:agentId/telegram/approve', auth, (req, res) => {
1223
+ const { agentId } = req.params;
1224
+ const apiKey = req.apiKey;
1225
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1226
+ res.status(403).json({ error: 'Admin key required' });
1227
+ return;
1228
+ }
1229
+ if (!agentConfigs.has(agentId)) {
1230
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1231
+ return;
1232
+ }
1233
+ const { code } = req.body;
1234
+ if (!code) {
1235
+ res.status(400).json({ error: 'code required' });
1236
+ return;
1237
+ }
1238
+ const access = readTelegramAccess(agentId);
1239
+ const entry = access.pending[code];
1240
+ if (!entry || entry.expiresAt < Date.now()) {
1241
+ res.status(404).json({ error: 'Pairing code not found or expired' });
1242
+ return;
1243
+ }
1244
+ if (!access.allowFrom.includes(entry.senderId))
1245
+ access.allowFrom.push(entry.senderId);
1246
+ delete access.pending[code];
1247
+ try {
1248
+ writeTelegramAccess(agentId, access);
1249
+ const approvedDir = path.join(getTelegramStateDir(agentId), 'approved');
1250
+ fs.mkdirSync(approvedDir, { recursive: true });
1251
+ fs.writeFileSync(path.join(approvedDir, entry.senderId), entry.chatId);
1252
+ }
1253
+ catch (err) {
1254
+ res.status(500).json({ error: `Failed to approve pairing: ${err.message}` });
1255
+ return;
1256
+ }
1257
+ res.json({ ok: true, senderId: entry.senderId });
1258
+ });
1259
+ /**
1260
+ * POST /api/v1/agents/:agentId/telegram/deny
1261
+ * Deny and remove a pending Telegram pairing by code.
1262
+ */
1263
+ router.post('/v1/agents/:agentId/telegram/deny', auth, (req, res) => {
1264
+ const { agentId } = req.params;
1265
+ const apiKey = req.apiKey;
1266
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1267
+ res.status(403).json({ error: 'Admin key required' });
1268
+ return;
1269
+ }
1270
+ if (!agentConfigs.has(agentId)) {
1271
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1272
+ return;
1273
+ }
1274
+ const { code } = req.body;
1275
+ if (!code) {
1276
+ res.status(400).json({ error: 'code required' });
1277
+ return;
1278
+ }
1279
+ const access = readTelegramAccess(agentId);
1280
+ if (!access.pending[code]) {
1281
+ res.status(404).json({ error: 'Pairing code not found' });
1282
+ return;
1283
+ }
1284
+ delete access.pending[code];
1285
+ try {
1286
+ writeTelegramAccess(agentId, access);
1287
+ }
1288
+ catch (err) {
1289
+ res.status(500).json({ error: `Failed to deny pairing: ${err.message}` });
1290
+ return;
1291
+ }
1292
+ res.json({ ok: true });
1293
+ });
1294
+ /**
1295
+ * POST /api/v1/agents/:agentId/telegram/init-pairing
1296
+ * Write sentinel file so the next private message auto-approves sender as owner.
1297
+ */
1298
+ router.post('/v1/agents/:agentId/telegram/init-pairing', auth, (req, res) => {
1299
+ const { agentId } = req.params;
1300
+ const apiKey = req.apiKey;
1301
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1302
+ res.status(403).json({ error: 'Admin key required' });
1303
+ return;
1304
+ }
1305
+ if (!agentConfigs.has(agentId)) {
1306
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1307
+ return;
1308
+ }
1309
+ const stateDir = getTelegramStateDir(agentId);
1310
+ try {
1311
+ fs.mkdirSync(stateDir, { recursive: true });
1312
+ fs.writeFileSync(path.join(stateDir, 'awaiting-owner'), '');
1313
+ }
1314
+ catch (err) {
1315
+ res.status(500).json({ error: `Failed to write sentinel: ${err.message}` });
1316
+ return;
1317
+ }
1318
+ res.json({ ok: true });
1319
+ });
1320
+ /**
1321
+ * GET /api/v1/agents/:agentId/telegram/pairing-status
1322
+ * Returns whether init-pairing sentinel is still active.
1323
+ */
1324
+ router.get('/v1/agents/:agentId/telegram/pairing-status', auth, (req, res) => {
1325
+ const { agentId } = req.params;
1326
+ const apiKey = req.apiKey;
1327
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1328
+ res.status(403).json({ error: 'Admin key required' });
1329
+ return;
1330
+ }
1331
+ if (!agentConfigs.has(agentId)) {
1332
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1333
+ return;
1334
+ }
1335
+ const sentinelPath = path.join(getTelegramStateDir(agentId), 'awaiting-owner');
1336
+ let waiting = false;
1337
+ try {
1338
+ const stat = fs.statSync(sentinelPath);
1339
+ waiting = Date.now() - stat.mtimeMs < 10 * 60 * 1000;
1340
+ if (!waiting)
1341
+ fs.rmSync(sentinelPath, { force: true });
1342
+ }
1343
+ catch { /* ENOENT — not waiting */ }
1344
+ const access = readTelegramAccess(agentId);
1345
+ res.json({ waiting, allowFrom: access.allowFrom });
1346
+ });
1347
+ /**
1348
+ * PATCH /api/v1/agents/:agentId/telegram/policy
1349
+ * Update the Telegram DM policy for an agent.
1350
+ */
1351
+ router.patch('/v1/agents/:agentId/telegram/policy', auth, (req, res) => {
1352
+ const { agentId } = req.params;
1353
+ const apiKey = req.apiKey;
1354
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1355
+ res.status(403).json({ error: 'Admin key required' });
1356
+ return;
1357
+ }
1358
+ if (!agentConfigs.has(agentId)) {
1359
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1360
+ return;
1361
+ }
1362
+ const { dmPolicy } = req.body;
1363
+ const valid = ['open', 'pairing', 'allowlist', 'disabled'];
1364
+ if (!dmPolicy || !valid.includes(dmPolicy)) {
1365
+ res.status(400).json({ error: `dmPolicy must be one of: ${valid.join(', ')}` });
1366
+ return;
1367
+ }
1368
+ const access = readTelegramAccess(agentId);
1369
+ access.dmPolicy = dmPolicy;
1370
+ try {
1371
+ writeTelegramAccess(agentId, access);
1372
+ }
1373
+ catch (err) {
1374
+ res.status(500).json({ error: `Failed to update policy: ${err.message}` });
1375
+ return;
1376
+ }
1377
+ res.json({ ok: true, dmPolicy });
1378
+ });
1379
+ /**
1380
+ * GET /api/v1/agents/:agentId/telegram/allowlist
1381
+ * Return all users in allowFrom for an agent's Telegram channel.
1382
+ */
1383
+ router.get('/v1/agents/:agentId/telegram/allowlist', auth, (req, res) => {
1384
+ const { agentId } = req.params;
1385
+ const apiKey = req.apiKey;
1386
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1387
+ res.status(403).json({ error: 'Admin key required' });
1388
+ return;
1389
+ }
1390
+ if (!agentConfigs.has(agentId)) {
1391
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1392
+ return;
1393
+ }
1394
+ const access = readTelegramAccess(agentId);
1395
+ res.json({ allowFrom: access.allowFrom });
1396
+ });
1397
+ /**
1398
+ * DELETE /api/v1/agents/:agentId/telegram/allow/:userId
1399
+ * Remove a user from the allowFrom list. Admin only.
1400
+ */
1401
+ router.delete('/v1/agents/:agentId/telegram/allow/:userId', auth, (req, res) => {
1402
+ const apiKey = req.apiKey;
1403
+ if (!(0, auth_1.isAdmin)(apiKey)) {
1404
+ res.status(403).json({ error: 'Admin key required' });
1405
+ return;
1406
+ }
1407
+ const { agentId, userId } = req.params;
1408
+ if (!/^\d+$/.test(userId)) {
1409
+ res.status(400).json({ error: 'Invalid userId: must be a numeric Telegram user ID' });
1410
+ return;
1411
+ }
1412
+ if (!agentConfigs.has(agentId)) {
1413
+ res.status(404).json({ error: `Agent '${agentId}' not found` });
1414
+ return;
1415
+ }
1416
+ const access = readTelegramAccess(agentId);
1417
+ access.allowFrom = access.allowFrom.filter((id) => id !== userId);
1418
+ try {
1419
+ writeTelegramAccess(agentId, access);
1420
+ }
1421
+ catch (err) {
1422
+ res.status(500).json({ error: `Failed to update allowlist: ${err.message}` });
1423
+ return;
1424
+ }
1425
+ res.json({ ok: true });
1045
1426
  });
1046
1427
  /**
1047
1428
  * DELETE /api/v1/agents/:agentId