@integrity-labs/agt-cli 0.19.18 → 0.19.20

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.
@@ -14217,6 +14217,9 @@ function classifyPeerMessage(msg, cfg, self) {
14217
14217
  if (self.bot_id !== null && msg.from.id === self.bot_id) {
14218
14218
  return { kind: "self" };
14219
14219
  }
14220
+ if (cfg.peer_disabled_mode === "all") {
14221
+ return { kind: "drop", reason: "peer_disabled_all" };
14222
+ }
14220
14223
  if (cfg.peer_agent_mode === "off") {
14221
14224
  return { kind: "drop", reason: "mode_off" };
14222
14225
  }
@@ -14232,16 +14235,30 @@ function classifyPeerMessage(msg, cfg, self) {
14232
14235
  if (!peer) {
14233
14236
  return { kind: "drop", reason: "unknown_peer" };
14234
14237
  }
14238
+ if (peer.gate_path === null) {
14239
+ return { kind: "drop", reason: "cross_team_grant_missing" };
14240
+ }
14241
+ if (cfg.peer_disabled_mode === "cross_team_only" && peer.gate_path !== void 0 && peer.gate_path !== "same_team") {
14242
+ return { kind: "drop", reason: "peer_disabled_cross_team" };
14243
+ }
14235
14244
  if (self.bot_id === null || self.bot_username === null) {
14236
14245
  return { kind: "drop", reason: "self_resolution_pending" };
14237
14246
  }
14238
- if (isAddressedToUs(msg, self.bot_id, self.bot_username)) {
14239
- return { kind: "peer-ingress", peer };
14247
+ const addressing = classifyAddressing(msg, self.bot_id, self.bot_username);
14248
+ if (!addressing.addressed) {
14249
+ return { kind: "drop", reason: "not_addressed" };
14250
+ }
14251
+ if (addressing.viaReplyOnly) {
14252
+ const inAllowedGroup = cfg.peer_group_ids.length > 0 && cfg.peer_group_ids.includes(chatId);
14253
+ const respondMode = cfg.peer_agent_mode === "respond";
14254
+ if (!(inAllowedGroup && respondMode)) {
14255
+ return { kind: "drop", reason: "not_addressed" };
14256
+ }
14240
14257
  }
14241
- return { kind: "drop", reason: "not_addressed" };
14258
+ return { kind: "peer-ingress", peer };
14242
14259
  }
14243
- function isAddressedToUs(msg, ourBotId, ourBotUsername) {
14244
- if (msg.reply_to_message?.from?.id === ourBotId) return true;
14260
+ function classifyAddressing(msg, ourBotId, ourBotUsername) {
14261
+ let viaMentionOrCommand = false;
14245
14262
  const text = msg.text ?? msg.caption ?? "";
14246
14263
  const entities = msg.entities ?? msg.caption_entities ?? [];
14247
14264
  const usAt = "@" + ourBotUsername;
@@ -14249,18 +14266,22 @@ function isAddressedToUs(msg, ourBotId, ourBotUsername) {
14249
14266
  const slice = text.slice(entity.offset, entity.offset + entity.length).toLowerCase();
14250
14267
  if (entity.type === "bot_command") {
14251
14268
  const at = slice.indexOf("@");
14252
- if (at < 0) {
14253
- return true;
14269
+ if (at < 0 || slice.slice(at + 1) === ourBotUsername) {
14270
+ viaMentionOrCommand = true;
14271
+ break;
14254
14272
  }
14255
- if (slice.slice(at + 1) === ourBotUsername) return true;
14256
14273
  } else if (entity.type === "mention" && slice === usAt) {
14257
- return true;
14274
+ viaMentionOrCommand = true;
14275
+ break;
14258
14276
  }
14259
14277
  }
14260
- return false;
14278
+ const viaReply = msg.reply_to_message?.from?.id === ourBotId;
14279
+ if (viaMentionOrCommand) return { addressed: true, viaReplyOnly: false };
14280
+ if (viaReply) return { addressed: true, viaReplyOnly: true };
14281
+ return { addressed: false, viaReplyOnly: false };
14261
14282
  }
14262
14283
  var CODE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14263
- function parsePeersEnv(raw) {
14284
+ function parsePeersEnv(raw, gateRaw) {
14264
14285
  if (!raw || raw.trim().length === 0) return [];
14265
14286
  let parsed;
14266
14287
  try {
@@ -14269,11 +14290,54 @@ function parsePeersEnv(raw) {
14269
14290
  return [];
14270
14291
  }
14271
14292
  if (!Array.isArray(parsed)) return [];
14293
+ const gateMap = /* @__PURE__ */ new Map();
14294
+ let gateConfigInvalid = false;
14295
+ if (gateRaw && gateRaw.trim().length > 0) {
14296
+ try {
14297
+ const gateParsed = JSON.parse(gateRaw);
14298
+ if (!gateParsed || typeof gateParsed !== "object" || Array.isArray(gateParsed)) {
14299
+ gateConfigInvalid = true;
14300
+ } else {
14301
+ for (const [k, v] of Object.entries(gateParsed)) {
14302
+ if (v === null) {
14303
+ gateMap.set(k, null);
14304
+ } else if (typeof v === "string") {
14305
+ const isGrantPath = v.startsWith("grant:") && v.slice("grant:".length).trim().length > 0;
14306
+ if (v === "same_team" || v === "intra_org_unrestricted" || isGrantPath) {
14307
+ gateMap.set(k, v);
14308
+ } else {
14309
+ gateConfigInvalid = true;
14310
+ break;
14311
+ }
14312
+ } else {
14313
+ gateConfigInvalid = true;
14314
+ break;
14315
+ }
14316
+ }
14317
+ }
14318
+ } catch {
14319
+ gateConfigInvalid = true;
14320
+ }
14321
+ if (gateConfigInvalid) {
14322
+ console.error(
14323
+ "[telegram-peer-classifier] TELEGRAM_PEERS_GATE is present but malformed; failing closed (every peer drops as cross_team_grant_missing)"
14324
+ );
14325
+ }
14326
+ }
14272
14327
  const out = [];
14273
14328
  for (const entry of parsed) {
14274
14329
  if (entry && typeof entry === "object" && typeof entry.code_name === "string" && CODE_NAME_RE.test(entry.code_name) && typeof entry.agent_id === "string" && typeof entry.bot_id === "number" && Number.isInteger(entry.bot_id) && entry.bot_id > 0) {
14275
14330
  const e = entry;
14276
- out.push({ code_name: e.code_name, bot_id: e.bot_id, agent_id: e.agent_id });
14331
+ const peer = { code_name: e.code_name, bot_id: e.bot_id, agent_id: e.agent_id };
14332
+ if (gateConfigInvalid) {
14333
+ peer.gate_path = null;
14334
+ } else {
14335
+ const key2 = String(e.bot_id);
14336
+ if (gateMap.has(key2)) {
14337
+ peer.gate_path = gateMap.get(key2) ?? null;
14338
+ }
14339
+ }
14340
+ out.push(peer);
14277
14341
  }
14278
14342
  }
14279
14343
  return out;
@@ -14287,6 +14351,388 @@ function parsePeerAgentModeEnv(raw) {
14287
14351
  return "off";
14288
14352
  }
14289
14353
 
14354
+ // src/telegram-peer-rate-limiter.ts
14355
+ var SECOND_MS = 1e3;
14356
+ var MINUTE_MS = 60 * SECOND_MS;
14357
+ var HOUR_MS = 60 * MINUTE_MS;
14358
+ var DAY_MS = 24 * HOUR_MS;
14359
+ var DEFAULT_RATE_LIMITER_CONFIG = Object.freeze({
14360
+ pairBurstLimit: 1,
14361
+ pairBurstWindowMs: 10 * SECOND_MS,
14362
+ pairWindowLimit: 5,
14363
+ pairWindowMs: 5 * MINUTE_MS,
14364
+ chatLimit: 10,
14365
+ chatWindowMs: 5 * MINUTE_MS,
14366
+ agentDailyLimit: 200,
14367
+ agentDailyWindowMs: DAY_MS,
14368
+ dailyBudgetLimit: 500,
14369
+ dailyBudgetWindowMs: DAY_MS
14370
+ });
14371
+ function readSliding(window, windowMs, now) {
14372
+ if (!window) return 0;
14373
+ const cutoff = now - windowMs;
14374
+ let i = 0;
14375
+ while (i < window.timestamps.length && window.timestamps[i] <= cutoff) i++;
14376
+ if (i > 0) window.timestamps.splice(0, i);
14377
+ return window.timestamps.length;
14378
+ }
14379
+ function bumpSliding(window, now) {
14380
+ window.timestamps.push(now);
14381
+ }
14382
+ function createPeerRateLimiter(config2 = DEFAULT_RATE_LIMITER_CONFIG) {
14383
+ const counters = {
14384
+ pairBurst: /* @__PURE__ */ new Map(),
14385
+ pairWindow: /* @__PURE__ */ new Map(),
14386
+ chat: /* @__PURE__ */ new Map(),
14387
+ agentDaily: { timestamps: [] },
14388
+ dailyBudget: { timestamps: [] }
14389
+ };
14390
+ let humanInbound = 0;
14391
+ function pairKey(chatId, peerBotId) {
14392
+ return `${chatId}:${peerBotId}`;
14393
+ }
14394
+ function readMap(map, key2, windowMs, now) {
14395
+ return readSliding(map.get(key2), windowMs, now);
14396
+ }
14397
+ function bumpMap(map, key2, now) {
14398
+ let entry = map.get(key2);
14399
+ if (!entry) {
14400
+ entry = { timestamps: [] };
14401
+ map.set(key2, entry);
14402
+ }
14403
+ bumpSliding(entry, now);
14404
+ }
14405
+ return {
14406
+ recordInboundPeer(chatId, peerBotId, now) {
14407
+ const pk = pairKey(chatId, peerBotId);
14408
+ if (readMap(counters.pairBurst, pk, config2.pairBurstWindowMs, now) >= config2.pairBurstLimit) {
14409
+ return { decision: "rate_limit_pair", dailyBudgetWarn: false };
14410
+ }
14411
+ if (readMap(counters.pairWindow, pk, config2.pairWindowMs, now) >= config2.pairWindowLimit) {
14412
+ return { decision: "rate_limit_pair", dailyBudgetWarn: false };
14413
+ }
14414
+ if (readMap(counters.chat, chatId, config2.chatWindowMs, now) >= config2.chatLimit) {
14415
+ return { decision: "rate_limit_chat", dailyBudgetWarn: false };
14416
+ }
14417
+ if (readSliding(counters.agentDaily, config2.agentDailyWindowMs, now) >= config2.agentDailyLimit) {
14418
+ return { decision: "rate_limit_agent_daily", dailyBudgetWarn: false };
14419
+ }
14420
+ const budgetSoFar = readSliding(counters.dailyBudget, config2.dailyBudgetWindowMs, now);
14421
+ if (budgetSoFar >= config2.dailyBudgetLimit) {
14422
+ return { decision: "daily_budget_exhausted", dailyBudgetWarn: false };
14423
+ }
14424
+ bumpMap(counters.pairBurst, pk, now);
14425
+ bumpMap(counters.pairWindow, pk, now);
14426
+ bumpMap(counters.chat, chatId, now);
14427
+ bumpSliding(counters.agentDaily, now);
14428
+ bumpSliding(counters.dailyBudget, now);
14429
+ const newBudget = budgetSoFar + 1;
14430
+ const warn80 = newBudget >= Math.ceil(config2.dailyBudgetLimit * 0.8) && newBudget < config2.dailyBudgetLimit;
14431
+ return { decision: "ok", dailyBudgetWarn: warn80 };
14432
+ },
14433
+ recordInboundHuman(_chatId, _now) {
14434
+ humanInbound += 1;
14435
+ },
14436
+ snapshot(now) {
14437
+ const perChat = /* @__PURE__ */ new Map();
14438
+ for (const [k, v] of counters.chat) {
14439
+ perChat.set(k, readSliding(v, config2.chatWindowMs, now));
14440
+ }
14441
+ const perPair = /* @__PURE__ */ new Map();
14442
+ for (const [k, v] of counters.pairWindow) {
14443
+ perPair.set(k, readSliding(v, config2.pairWindowMs, now));
14444
+ }
14445
+ return {
14446
+ perAgentDaily: readSliding(counters.agentDaily, config2.agentDailyWindowMs, now),
14447
+ dailyBudget: readSliding(counters.dailyBudget, config2.dailyBudgetWindowMs, now),
14448
+ perChat,
14449
+ perPair,
14450
+ humanInbound
14451
+ };
14452
+ }
14453
+ };
14454
+ }
14455
+ function createPresenceTracker() {
14456
+ const lastHumanByChat = /* @__PURE__ */ new Map();
14457
+ return {
14458
+ noteInbound(chatId, kind, now) {
14459
+ if (kind === "human") lastHumanByChat.set(chatId, now);
14460
+ },
14461
+ shouldDropOutgress(chatId, now, maxAgeMs = 30 * MINUTE_MS) {
14462
+ const last = lastHumanByChat.get(chatId);
14463
+ if (last === void 0) return "no_human_recent";
14464
+ if (now - last >= maxAgeMs) return "no_human_recent";
14465
+ return null;
14466
+ }
14467
+ };
14468
+ }
14469
+
14470
+ // src/telegram-peer-rate-limiter-db.ts
14471
+ function createDbBackedPeerRateLimiter(deps) {
14472
+ const config2 = deps.config ?? DEFAULT_RATE_LIMITER_CONFIG;
14473
+ const readCacheMs = deps.readCacheMs ?? 2e3;
14474
+ const log = deps.log ?? (() => {
14475
+ });
14476
+ const caches = /* @__PURE__ */ new Map();
14477
+ let humanInbound = 0;
14478
+ const pendingWrites = /* @__PURE__ */ new Set();
14479
+ function trackPending(write) {
14480
+ pendingWrites.add(write);
14481
+ void write.finally(() => pendingWrites.delete(write));
14482
+ }
14483
+ function cacheKey2(layer, chatId, peerBotId) {
14484
+ return `${layer}|${chatId}|${peerBotId ?? ""}`;
14485
+ }
14486
+ const localBumpTimes = /* @__PURE__ */ new Map();
14487
+ function bumpLocal(layer, chatId, peerBotId, now) {
14488
+ const key2 = cacheKey2(layer, chatId, peerBotId);
14489
+ const list = localBumpTimes.get(key2);
14490
+ if (list) list.push(now);
14491
+ else localBumpTimes.set(key2, [now]);
14492
+ }
14493
+ function localBumpsSince(key2, fetchedAt) {
14494
+ const list = localBumpTimes.get(key2);
14495
+ if (!list) return 0;
14496
+ let n = 0;
14497
+ for (let i = list.length - 1; i >= 0; i--) {
14498
+ if (list[i] >= fetchedAt) n++;
14499
+ else break;
14500
+ }
14501
+ return n;
14502
+ }
14503
+ function pruneLocalBumps(retentionMs, now) {
14504
+ const cutoff = now - retentionMs;
14505
+ for (const [key2, list] of localBumpTimes) {
14506
+ let i = 0;
14507
+ while (i < list.length && list[i] < cutoff) i++;
14508
+ if (i > 0) list.splice(0, i);
14509
+ if (list.length === 0) localBumpTimes.delete(key2);
14510
+ }
14511
+ }
14512
+ async function readCount(layer, chatId, peerBotId, windowMs, now) {
14513
+ const key2 = cacheKey2(layer, chatId, peerBotId);
14514
+ const cached2 = caches.get(key2);
14515
+ if (cached2 && now - cached2.fetchedAt < readCacheMs) {
14516
+ return cached2.count + localBumpsSince(key2, cached2.fetchedAt);
14517
+ }
14518
+ const since = new Date(now - windowMs).toISOString();
14519
+ let count;
14520
+ try {
14521
+ const args = {
14522
+ since,
14523
+ traffic_class: layer
14524
+ };
14525
+ if (layer === "pair_burst" || layer === "pair_window") {
14526
+ args.chat_id = chatId;
14527
+ args.peer_bot_id = peerBotId ?? void 0;
14528
+ } else if (layer === "chat") {
14529
+ args.chat_id = chatId;
14530
+ }
14531
+ count = await deps.api.countEvents(args);
14532
+ } catch (err) {
14533
+ log(`peer_rate_event read failed (layer=${layer}): ${err.message} \u2014 fail-open`);
14534
+ count = 0;
14535
+ }
14536
+ const fetchedAt = now;
14537
+ caches.set(key2, { fetchedAt, count });
14538
+ return count + localBumpsSince(key2, fetchedAt);
14539
+ }
14540
+ async function decide(chatId, peerBotId, now) {
14541
+ const layers = [
14542
+ { layer: "pair_burst", chatId, peerBotId, limit: config2.pairBurstLimit, windowMs: config2.pairBurstWindowMs, reason: "rate_limit_pair" },
14543
+ { layer: "pair_window", chatId, peerBotId, limit: config2.pairWindowLimit, windowMs: config2.pairWindowMs, reason: "rate_limit_pair" },
14544
+ { layer: "chat", chatId, peerBotId: null, limit: config2.chatLimit, windowMs: config2.chatWindowMs, reason: "rate_limit_chat" },
14545
+ { layer: "agent_daily", chatId: "", peerBotId: null, limit: config2.agentDailyLimit, windowMs: config2.agentDailyWindowMs, reason: "rate_limit_agent_daily" },
14546
+ { layer: "daily_budget", chatId: "", peerBotId: null, limit: config2.dailyBudgetLimit, windowMs: config2.dailyBudgetWindowMs, reason: "daily_budget_exhausted" }
14547
+ ];
14548
+ for (const l of layers) {
14549
+ const count = await readCount(l.layer, l.chatId, l.peerBotId, l.windowMs, now);
14550
+ if (count >= l.limit) {
14551
+ return { decision: l.reason, dailyBudgetWarn: false };
14552
+ }
14553
+ }
14554
+ for (const l of layers) bumpLocal(l.layer, l.chatId, l.peerBotId, now);
14555
+ pruneLocalBumps(Math.max(config2.pairBurstWindowMs, 6e4), now);
14556
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: peerBotId, traffic_class: "pair_burst" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14557
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: peerBotId, traffic_class: "pair_window" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14558
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: null, traffic_class: "chat" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14559
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: null, traffic_class: "agent_daily" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14560
+ trackPending(deps.api.recordEvent({ chat_id: chatId, peer_bot_id: null, traffic_class: "daily_budget" }).catch((err) => log(`peer_rate_event write failed: ${err.message}`)));
14561
+ const dbKey = cacheKey2("daily_budget", "", null);
14562
+ const dbCached = caches.get(dbKey);
14563
+ const dbBudget = (dbCached?.count ?? 0) + localBumpsSince(dbKey, dbCached?.fetchedAt ?? 0);
14564
+ const warn80 = dbBudget >= Math.ceil(config2.dailyBudgetLimit * 0.8) && dbBudget < config2.dailyBudgetLimit;
14565
+ return { decision: "ok", dailyBudgetWarn: warn80 };
14566
+ }
14567
+ return {
14568
+ recordInboundPeer(chatId, peerBotId, now) {
14569
+ return decide(chatId, peerBotId, now);
14570
+ },
14571
+ recordInboundHuman(_chatId, _now) {
14572
+ humanInbound += 1;
14573
+ },
14574
+ snapshot(now) {
14575
+ const perChat = /* @__PURE__ */ new Map();
14576
+ const perPair = /* @__PURE__ */ new Map();
14577
+ let perAgentDaily = 0;
14578
+ let dailyBudget = 0;
14579
+ for (const [k, v] of caches) {
14580
+ const local = localBumpsSince(k, v.fetchedAt);
14581
+ const layer = k.split("|")[0];
14582
+ if (layer === "pair_window") {
14583
+ const chatId = k.split("|")[1];
14584
+ const peerBotId = k.split("|")[2];
14585
+ perPair.set(`${chatId}:${peerBotId}`, v.count + local);
14586
+ } else if (layer === "chat") {
14587
+ const chatId = k.split("|")[1];
14588
+ perChat.set(chatId, v.count + local);
14589
+ } else if (layer === "agent_daily") {
14590
+ perAgentDaily = v.count + local;
14591
+ } else if (layer === "daily_budget") {
14592
+ dailyBudget = v.count + local;
14593
+ }
14594
+ }
14595
+ void now;
14596
+ return { perAgentDaily, dailyBudget, perChat, perPair, humanInbound };
14597
+ },
14598
+ async flushPending() {
14599
+ await Promise.all([...pendingWrites]);
14600
+ }
14601
+ };
14602
+ }
14603
+ function createDefaultPeerRateApiClient(args) {
14604
+ if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
14605
+ const fetchImpl = args.fetchImpl ?? fetch;
14606
+ const base = args.agtHost.replace(/\/+$/, "");
14607
+ let cachedToken = null;
14608
+ let cachedTokenExpiresAt = 0;
14609
+ async function getToken() {
14610
+ if (cachedToken && Date.now() < cachedTokenExpiresAt) return cachedToken;
14611
+ const resp = await fetchImpl(`${base}/host/exchange`, {
14612
+ method: "POST",
14613
+ headers: { "Content-Type": "application/json" },
14614
+ body: JSON.stringify({ host_key: args.agtApiKey })
14615
+ });
14616
+ if (!resp.ok) {
14617
+ const body = await resp.text().catch(() => "");
14618
+ throw new Error(`/host/exchange failed (${resp.status}): ${body.slice(0, 200)}`);
14619
+ }
14620
+ const data = await resp.json();
14621
+ cachedToken = data.token;
14622
+ cachedTokenExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() - 12e4 : Date.now() + 55 * 6e4;
14623
+ return cachedToken;
14624
+ }
14625
+ async function authedFetch(path, init) {
14626
+ const headers = {
14627
+ Authorization: `Bearer ${await getToken()}`
14628
+ };
14629
+ if (init.body !== void 0) headers["Content-Type"] = "application/json";
14630
+ let resp = await fetchImpl(`${base}${path}`, { method: init.method, headers, body: init.body });
14631
+ if (resp.status === 401) {
14632
+ cachedToken = null;
14633
+ cachedTokenExpiresAt = 0;
14634
+ const retryHeaders = {
14635
+ Authorization: `Bearer ${await getToken()}`
14636
+ };
14637
+ if (init.body !== void 0) retryHeaders["Content-Type"] = "application/json";
14638
+ resp = await fetchImpl(`${base}${path}`, { method: init.method, headers: retryHeaders, body: init.body });
14639
+ }
14640
+ return resp;
14641
+ }
14642
+ return {
14643
+ async recordEvent({ chat_id, peer_bot_id, traffic_class }) {
14644
+ const resp = await authedFetch("/host/peer-rate-events", {
14645
+ method: "POST",
14646
+ body: JSON.stringify({ agent_id: args.agentId, chat_id, peer_bot_id, traffic_class })
14647
+ });
14648
+ if (!resp.ok) {
14649
+ throw new Error(`POST /host/peer-rate-events failed: ${resp.status} ${await resp.text()}`);
14650
+ }
14651
+ },
14652
+ async countEvents({ since, chat_id, peer_bot_id, traffic_class }) {
14653
+ const params = new URLSearchParams({
14654
+ agent_id: args.agentId,
14655
+ since
14656
+ });
14657
+ if (chat_id !== void 0) params.set("chat_id", chat_id);
14658
+ if (peer_bot_id !== void 0) params.set("peer_bot_id", String(peer_bot_id));
14659
+ if (traffic_class !== void 0) params.set("traffic_class", traffic_class);
14660
+ const resp = await authedFetch(`/host/peer-rate-events?${params}`, { method: "GET" });
14661
+ if (!resp.ok) {
14662
+ throw new Error(`GET /host/peer-rate-events failed: ${resp.status} ${await resp.text()}`);
14663
+ }
14664
+ const json = await resp.json();
14665
+ return typeof json.count === "number" ? json.count : 0;
14666
+ }
14667
+ };
14668
+ }
14669
+
14670
+ // src/cross-team-peer-audit-client.ts
14671
+ var REQUEST_TIMEOUT_MS = 1e4;
14672
+ function createCrossTeamPeerAuditClient(args) {
14673
+ if (!args.agtHost || !args.agtApiKey || !args.agentId) return null;
14674
+ const fetchImpl = args.fetchImpl ?? fetch;
14675
+ const log = args.log ?? (() => {
14676
+ });
14677
+ const base = args.agtHost.replace(/\/+$/, "");
14678
+ const agentId = args.agentId;
14679
+ const apiKey = args.agtApiKey;
14680
+ let cachedToken = null;
14681
+ let cachedTokenExpiresAt = 0;
14682
+ async function getToken() {
14683
+ if (cachedToken && Date.now() < cachedTokenExpiresAt) return cachedToken;
14684
+ const resp = await fetchImpl(`${base}/host/exchange`, {
14685
+ method: "POST",
14686
+ headers: { "Content-Type": "application/json" },
14687
+ body: JSON.stringify({ host_key: apiKey }),
14688
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
14689
+ });
14690
+ if (!resp.ok) {
14691
+ const body = await resp.text().catch(() => "");
14692
+ throw new Error(`/host/exchange failed (${resp.status}): ${body.slice(0, 200)}`);
14693
+ }
14694
+ const data = await resp.json();
14695
+ cachedToken = data.token;
14696
+ cachedTokenExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() - 12e4 : Date.now() + 55 * 6e4;
14697
+ return cachedToken;
14698
+ }
14699
+ async function postOnce(event) {
14700
+ const token = await getToken();
14701
+ return fetchImpl(`${base}/host/cross-team-peer-event`, {
14702
+ method: "POST",
14703
+ headers: {
14704
+ Authorization: `Bearer ${token}`,
14705
+ "Content-Type": "application/json"
14706
+ },
14707
+ body: JSON.stringify({ agent_id: agentId, ...event }),
14708
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
14709
+ });
14710
+ }
14711
+ async function emitAsync(event) {
14712
+ try {
14713
+ let resp = await postOnce(event);
14714
+ if (resp.status === 401) {
14715
+ cachedToken = null;
14716
+ cachedTokenExpiresAt = 0;
14717
+ resp = await postOnce(event);
14718
+ }
14719
+ if (!resp.ok) {
14720
+ const body = await resp.text().catch(() => "");
14721
+ log(
14722
+ `cross-team-peer-audit: POST failed (${resp.status}) event=${event.event_type}: ${body.slice(0, 200)}`
14723
+ );
14724
+ }
14725
+ } catch (err) {
14726
+ log(`cross-team-peer-audit: emit threw event=${event.event_type}: ${err.message}`);
14727
+ }
14728
+ }
14729
+ return {
14730
+ emit(event) {
14731
+ void emitAsync(event);
14732
+ }
14733
+ };
14734
+ }
14735
+
14290
14736
  // src/telegram-channel.ts
14291
14737
  function redactId(id) {
14292
14738
  return createHash("sha256").update(String(id)).digest("hex").slice(0, 8);
@@ -14301,8 +14747,50 @@ var ALLOWED_CHATS = new Set(
14301
14747
  var PEER_CLASSIFIER_CONFIG = {
14302
14748
  peer_agent_mode: parsePeerAgentModeEnv(process.env.TELEGRAM_PEER_AGENT_MODE),
14303
14749
  peer_group_ids: parsePeerGroupIdsEnv(process.env.TELEGRAM_PEER_GROUP_IDS),
14304
- peers: parsePeersEnv(process.env.TELEGRAM_PEERS)
14750
+ peers: parsePeersEnv(process.env.TELEGRAM_PEERS, process.env.TELEGRAM_PEERS_GATE),
14751
+ peer_disabled_mode: "off"
14752
+ // populated below from PEER_DISABLED_MODE
14305
14753
  };
14754
+ var PEER_DISABLED_MODE = (() => {
14755
+ const next = (process.env.PEER_DISABLED ?? "").trim().toLowerCase();
14756
+ if (next === "off" || next === "cross_team_only" || next === "all") return next;
14757
+ if (next.length > 0) {
14758
+ process.stderr.write(
14759
+ `telegram-channel(${AGENT_CODE_NAME}): invalid PEER_DISABLED=${JSON.stringify(process.env.PEER_DISABLED)} \u2014 defaulting to 'all' (fail-closed)
14760
+ `
14761
+ );
14762
+ return "all";
14763
+ }
14764
+ const legacy = (process.env.TELEGRAM_PEER_DISABLED ?? "").trim().toLowerCase();
14765
+ return legacy === "1" || legacy === "true" || legacy === "yes" ? "all" : "off";
14766
+ })();
14767
+ var PEER_DISABLED_GLOBAL = PEER_DISABLED_MODE === "all";
14768
+ if (PEER_DISABLED_MODE !== "off") {
14769
+ process.stderr.write(
14770
+ `telegram-channel(${AGENT_CODE_NAME}): PEER_DISABLED=${PEER_DISABLED_MODE} \u2014 ${PEER_DISABLED_MODE === "all" ? "every peer surface short-circuited" : "cross-team peers dropped, same-team peers admitted"}
14771
+ `
14772
+ );
14773
+ }
14774
+ PEER_CLASSIFIER_CONFIG.peer_disabled_mode = PEER_DISABLED_MODE;
14775
+ var crossTeamPeerAuditClient = createCrossTeamPeerAuditClient({
14776
+ agtHost: AGT_HOST,
14777
+ agtApiKey: AGT_API_KEY,
14778
+ agentId: process.env.AGT_AGENT_ID ?? null,
14779
+ log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
14780
+ `)
14781
+ });
14782
+ var peerRateApiClient = createDefaultPeerRateApiClient({
14783
+ agtHost: AGT_HOST,
14784
+ agtApiKey: AGT_API_KEY,
14785
+ agentId: process.env.AGT_AGENT_ID ?? null
14786
+ });
14787
+ var peerRateLimiter = peerRateApiClient && parsePeerAgentModeEnv(process.env.TELEGRAM_PEER_AGENT_MODE) !== "off" ? createDbBackedPeerRateLimiter({
14788
+ api: peerRateApiClient,
14789
+ log: (line) => process.stderr.write(`telegram-channel(${AGENT_CODE_NAME}): ${line}
14790
+ `)
14791
+ }) : createPeerRateLimiter();
14792
+ var peerPresence = createPresenceTracker();
14793
+ var dailyBudgetWarned = false;
14306
14794
  if (!BOT_TOKEN) {
14307
14795
  process.stderr.write(
14308
14796
  "telegram-channel: Missing TELEGRAM_BOT_TOKEN. Cannot start.\n"
@@ -14939,6 +15427,40 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
14939
15427
  isError: true
14940
15428
  };
14941
15429
  }
15430
+ const peerActiveOnChat = PEER_CLASSIFIER_CONFIG.peer_agent_mode !== "off" && PEER_CLASSIFIER_CONFIG.peer_group_ids.length > 0 && PEER_CLASSIFIER_CONFIG.peer_group_ids.includes(chat_id);
15431
+ if (peerActiveOnChat) {
15432
+ if (PEER_DISABLED_GLOBAL) {
15433
+ process.stderr.write(
15434
+ `telegram-channel(${AGENT_CODE_NAME}): outgress_blocked reason=peer_disabled_global chat=${redactId(chat_id)}
15435
+ `
15436
+ );
15437
+ return {
15438
+ content: [
15439
+ {
15440
+ type: "text",
15441
+ text: "Multi-agent collaboration is disabled (TELEGRAM_PEER_DISABLED). Stop attempting to reply on this chat."
15442
+ }
15443
+ ],
15444
+ isError: true
15445
+ };
15446
+ }
15447
+ const stale = peerPresence.shouldDropOutgress(chat_id, Date.now());
15448
+ if (stale === "no_human_recent") {
15449
+ process.stderr.write(
15450
+ `telegram-channel(${AGENT_CODE_NAME}): outgress_blocked reason=no_human_recent chat=${redactId(chat_id)}
15451
+ `
15452
+ );
15453
+ return {
15454
+ content: [
15455
+ {
15456
+ type: "text",
15457
+ text: "No human message in this chat in the last 30 minutes. Peer-agent collaboration is paused for this chat until a human posts."
15458
+ }
15459
+ ],
15460
+ isError: true
15461
+ };
15462
+ }
15463
+ }
14942
15464
  const killed = await isThreadKilled({
14943
15465
  channelType: "telegram",
14944
15466
  channelId: chat_id,
@@ -15245,11 +15767,23 @@ async function pollLoop() {
15245
15767
  const userId = msg.from?.id != null ? String(msg.from.id) : "unknown";
15246
15768
  const userName = msg.from?.username || [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || userId;
15247
15769
  const isFromBot = !!msg.from?.is_bot;
15770
+ const nowMs = Date.now();
15248
15771
  if (chatId && !isFromBot) {
15249
15772
  resetThread(chatId, "");
15250
15773
  }
15774
+ peerPresence.noteInbound(chatId, isFromBot ? "bot" : "human", nowMs);
15775
+ if (!isFromBot) {
15776
+ peerRateLimiter.recordInboundHuman(chatId, nowMs);
15777
+ }
15251
15778
  let peerAgentMeta = null;
15252
15779
  if (isFromBot) {
15780
+ if (PEER_DISABLED_GLOBAL) {
15781
+ process.stderr.write(
15782
+ `telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=peer_disabled_global chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
15783
+ `
15784
+ );
15785
+ continue;
15786
+ }
15253
15787
  const classifierMsg = {
15254
15788
  message_id: msg.message_id,
15255
15789
  text: msg.text,
@@ -15273,10 +15807,62 @@ async function pollLoop() {
15273
15807
  `telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=${classification.reason} chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
15274
15808
  `
15275
15809
  );
15810
+ if (classification.reason === "cross_team_grant_missing") {
15811
+ crossTeamPeerAuditClient?.emit({
15812
+ event_type: "telegram.peer.drop.cross_team_grant_missing",
15813
+ peer_agent_id: null,
15814
+ gate_path: null,
15815
+ grant_id: null,
15816
+ chat_id: String(chatId),
15817
+ message_id: String(msg.message_id)
15818
+ });
15819
+ }
15276
15820
  continue;
15277
15821
  }
15278
15822
  if (classification.kind === "peer-ingress") {
15279
- peerAgentMeta = { code_name: classification.peer.code_name, agent_id: classification.peer.agent_id };
15823
+ const limit = await peerRateLimiter.recordInboundPeer(
15824
+ chatId,
15825
+ classification.peer.bot_id,
15826
+ nowMs
15827
+ );
15828
+ if (limit.decision !== "ok") {
15829
+ process.stderr.write(
15830
+ `telegram-channel(${AGENT_CODE_NAME}): peer_drop reason=${limit.decision} chat=${redactId(chatId)} from=${redactId(String(msg.from?.id ?? "unknown"))}
15831
+ `
15832
+ );
15833
+ continue;
15834
+ }
15835
+ if (limit.dailyBudgetWarn && !dailyBudgetWarned) {
15836
+ dailyBudgetWarned = true;
15837
+ process.stderr.write(
15838
+ `telegram-channel(${AGENT_CODE_NAME}): peer_warn reason=daily_budget_80pct
15839
+ `
15840
+ );
15841
+ }
15842
+ peerAgentMeta = {
15843
+ code_name: classification.peer.code_name,
15844
+ agent_id: classification.peer.agent_id
15845
+ };
15846
+ const gate = classification.peer.gate_path;
15847
+ if (gate && gate !== "same_team" && gate !== "intra_org_unrestricted" && gate.startsWith("grant:")) {
15848
+ crossTeamPeerAuditClient?.emit({
15849
+ event_type: "telegram.peer.ingress.cross_team",
15850
+ peer_agent_id: classification.peer.agent_id || null,
15851
+ gate_path: gate,
15852
+ grant_id: gate.slice("grant:".length),
15853
+ chat_id: String(chatId),
15854
+ message_id: String(msg.message_id)
15855
+ });
15856
+ } else if (gate === "intra_org_unrestricted") {
15857
+ crossTeamPeerAuditClient?.emit({
15858
+ event_type: "telegram.peer.ingress.cross_team",
15859
+ peer_agent_id: classification.peer.agent_id || null,
15860
+ gate_path: gate,
15861
+ grant_id: null,
15862
+ chat_id: String(chatId),
15863
+ message_id: String(msg.message_id)
15864
+ });
15865
+ }
15280
15866
  }
15281
15867
  }
15282
15868
  const messageId = String(msg.message_id);