switchroom 0.14.9 → 0.14.10

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.
@@ -11091,6 +11091,7 @@ var TelegramChannelSchema = exports_external.object({
11091
11091
  webhook_rate_limit: exports_external.object({
11092
11092
  rpm: exports_external.number().int().positive()
11093
11093
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11094
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11094
11095
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11095
11096
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11096
11097
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
@@ -12120,6 +12121,52 @@ function validateAllCronTopicAliases(config, filePath) {
12120
12121
  }
12121
12122
  }
12122
12123
 
12124
+ // src/scheduler/dispatch.ts
12125
+ import { createHash } from "node:crypto";
12126
+ function collectScheduleEntries(config) {
12127
+ const out = [];
12128
+ const agentNames = Object.keys(config.agents).sort();
12129
+ for (const agent of agentNames) {
12130
+ const raw = config.agents[agent];
12131
+ if (!raw)
12132
+ continue;
12133
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, raw);
12134
+ const schedule = resolved.schedule ?? [];
12135
+ for (let i = 0;i < schedule.length; i++) {
12136
+ const entry = schedule[i];
12137
+ out.push({
12138
+ agent,
12139
+ scheduleIndex: i,
12140
+ cron: entry.cron,
12141
+ prompt: entry.prompt,
12142
+ promptKey: createHash("sha256").update(entry.prompt).digest("hex").slice(0, 12),
12143
+ ...entry.topic !== undefined ? { topic: entry.topic } : {}
12144
+ });
12145
+ }
12146
+ }
12147
+ return out;
12148
+ }
12149
+ function dispatchAsInbound(entry, options, dispatcher) {
12150
+ const ts = (options.now ?? Date.now)();
12151
+ const message = {
12152
+ type: "inbound",
12153
+ chatId: options.chatId,
12154
+ ...options.threadId !== undefined ? { threadId: options.threadId } : {},
12155
+ messageId: ts,
12156
+ user: "cron",
12157
+ userId: 0,
12158
+ ts,
12159
+ text: entry.prompt,
12160
+ meta: {
12161
+ source: "cron",
12162
+ schedule_index: String(entry.scheduleIndex),
12163
+ prompt_key: entry.promptKey
12164
+ }
12165
+ };
12166
+ const delivered = dispatcher.sendToAgent(entry.agent, message);
12167
+ return { delivered, message };
12168
+ }
12169
+
12123
12170
  // src/telegram/topic-router.ts
12124
12171
  var ALERTS_ALIAS = "alerts";
12125
12172
  var ADMIN_ALIAS = "admin";
@@ -12173,50 +12220,41 @@ function resolveOutboundTopic(config, event) {
12173
12220
  }
12174
12221
  }
12175
12222
 
12176
- // src/scheduler/dispatch.ts
12177
- import { createHash } from "node:crypto";
12178
- function collectScheduleEntries(config) {
12179
- const out = [];
12180
- const agentNames = Object.keys(config.agents).sort();
12181
- for (const agent of agentNames) {
12182
- const raw = config.agents[agent];
12183
- if (!raw)
12184
- continue;
12185
- const resolved = resolveAgentConfig(config.defaults, config.profiles, raw);
12186
- const schedule = resolved.schedule ?? [];
12187
- for (let i = 0;i < schedule.length; i++) {
12188
- const entry = schedule[i];
12189
- out.push({
12190
- agent,
12191
- scheduleIndex: i,
12192
- cron: entry.cron,
12193
- prompt: entry.prompt,
12194
- promptKey: createHash("sha256").update(entry.prompt).digest("hex").slice(0, 12),
12195
- ...entry.topic !== undefined ? { topic: entry.topic } : {}
12196
- });
12197
- }
12223
+ // src/agent-scheduler/channel-target.ts
12224
+ function resolveChannelTarget(config, agentName) {
12225
+ const agent = config.agents?.[agentName];
12226
+ const tgChannel = agent ? resolveAgentConfig(config.defaults, config.profiles, agent).channels?.telegram : undefined;
12227
+ const supergroupChatId = tgChannel?.chat_id;
12228
+ const supergroupDefaultTopic = tgChannel?.default_topic_id;
12229
+ if (typeof supergroupChatId === "string" && supergroupChatId.length > 0) {
12230
+ return {
12231
+ chatId: supergroupChatId,
12232
+ ...typeof supergroupDefaultTopic === "number" ? { threadId: supergroupDefaultTopic } : {},
12233
+ routerConfig: {
12234
+ ...typeof supergroupDefaultTopic === "number" ? { default_topic_id: supergroupDefaultTopic } : {},
12235
+ ...tgChannel?.topic_aliases ? { topic_aliases: tgChannel.topic_aliases } : {}
12236
+ }
12237
+ };
12198
12238
  }
12199
- return out;
12200
- }
12201
- function dispatchAsInbound(entry, options, dispatcher) {
12202
- const ts = (options.now ?? Date.now)();
12203
- const message = {
12204
- type: "inbound",
12205
- chatId: options.chatId,
12206
- ...options.threadId !== undefined ? { threadId: options.threadId } : {},
12207
- messageId: ts,
12208
- user: "cron",
12209
- userId: 0,
12210
- ts,
12211
- text: entry.prompt,
12212
- meta: {
12213
- source: "cron",
12214
- schedule_index: String(entry.scheduleIndex),
12215
- prompt_key: entry.promptKey
12216
- }
12239
+ const forumChatId = config.telegram?.forum_chat_id;
12240
+ if (typeof forumChatId !== "string" || forumChatId.length === 0)
12241
+ return null;
12242
+ const threadId = agent?.topic_id;
12243
+ return {
12244
+ chatId: forumChatId,
12245
+ ...typeof threadId === "number" ? { threadId } : {}
12217
12246
  };
12218
- const delivered = dispatcher.sendToAgent(entry.agent, message);
12219
- return { delivered, message };
12247
+ }
12248
+ function resolveEntryThreadId(entry, channel) {
12249
+ if (channel.routerConfig) {
12250
+ const routed = resolveOutboundTopic(channel.routerConfig, {
12251
+ kind: "cron",
12252
+ entryTopic: entry.topic
12253
+ });
12254
+ if (routed !== undefined)
12255
+ return routed;
12256
+ }
12257
+ return channel.threadId;
12220
12258
  }
12221
12259
 
12222
12260
  // src/scheduler/audit.ts
@@ -12716,41 +12754,6 @@ function ipcDispatcher(client) {
12716
12754
  }
12717
12755
  };
12718
12756
  }
12719
- function resolveChannelTarget(config, agentName) {
12720
- const agent = config.agents?.[agentName];
12721
- const tgChannel = agent ? resolveAgentConfig(config.defaults, config.profiles, agent).channels?.telegram : undefined;
12722
- const supergroupChatId = tgChannel?.chat_id;
12723
- const supergroupDefaultTopic = tgChannel?.default_topic_id;
12724
- if (typeof supergroupChatId === "string" && supergroupChatId.length > 0) {
12725
- return {
12726
- chatId: supergroupChatId,
12727
- ...typeof supergroupDefaultTopic === "number" ? { threadId: supergroupDefaultTopic } : {},
12728
- routerConfig: {
12729
- ...typeof supergroupDefaultTopic === "number" ? { default_topic_id: supergroupDefaultTopic } : {},
12730
- ...tgChannel?.topic_aliases ? { topic_aliases: tgChannel.topic_aliases } : {}
12731
- }
12732
- };
12733
- }
12734
- const forumChatId = config.telegram?.forum_chat_id;
12735
- if (typeof forumChatId !== "string" || forumChatId.length === 0)
12736
- return null;
12737
- const threadId = agent?.topic_id;
12738
- return {
12739
- chatId: forumChatId,
12740
- ...typeof threadId === "number" ? { threadId } : {}
12741
- };
12742
- }
12743
- function resolveEntryThreadId(entry, channel) {
12744
- if (channel.routerConfig) {
12745
- const routed = resolveOutboundTopic(channel.routerConfig, {
12746
- kind: "cron",
12747
- entryTopic: entry.topic
12748
- });
12749
- if (routed !== undefined)
12750
- return routed;
12751
- }
12752
- return channel.threadId;
12753
- }
12754
12757
  async function main() {
12755
12758
  const agentName = process.env.SWITCHROOM_AGENT_NAME;
12756
12759
  if (!agentName) {
@@ -11091,6 +11091,7 @@ var TelegramChannelSchema = exports_external.object({
11091
11091
  webhook_rate_limit: exports_external.object({
11092
11092
  rpm: exports_external.number().int().positive()
11093
11093
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11094
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11094
11095
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11095
11096
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11096
11097
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
@@ -11838,6 +11838,7 @@ var TelegramChannelSchema = exports_external.object({
11838
11838
  webhook_rate_limit: exports_external.object({
11839
11839
  rpm: exports_external.number().int().positive()
11840
11840
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11841
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11841
11842
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11842
11843
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11843
11844
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
@@ -13655,6 +13655,7 @@ var init_schema = __esm(() => {
13655
13655
  webhook_rate_limit: exports_external.object({
13656
13656
  rpm: exports_external.number().int().positive()
13657
13657
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
13658
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
13658
13659
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
13659
13660
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
13660
13661
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
@@ -23313,7 +23314,7 @@ function generateCompose(opts) {
23313
23314
  telemetryDisabled,
23314
23315
  posthogKeyOverride,
23315
23316
  posthogHostOverride
23316
- }, bundledSkillsPoolDir, hostControlEnabled);
23317
+ }, bundledSkillsPoolDir, hostControlEnabled, opts.operatorUid);
23317
23318
  }
23318
23319
  lines.push(`volumes:`);
23319
23320
  for (const a of describeAgents(config)) {
@@ -23338,7 +23339,7 @@ function generateCompose(opts) {
23338
23339
  return lines.join(`
23339
23340
  `);
23340
23341
  }
23341
- function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefix, hostHomeForChecks, switchroomConfigPath, containerNamePrefix, posthog, bundledSkillsPoolDir, hostControlEnabled) {
23342
+ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefix, hostHomeForChecks, switchroomConfigPath, containerNamePrefix, posthog, bundledSkillsPoolDir, hostControlEnabled, operatorUid) {
23342
23343
  lines.push(` agent-${a.name}:`);
23343
23344
  emitImageOrBuild(lines, "agent", imageTag, buildMode, buildContext);
23344
23345
  lines.push(` container_name: ${containerNamePrefix}-${a.name}`);
@@ -23418,6 +23419,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23418
23419
  if (a.admin === true) {
23419
23420
  env2.SWITCHROOM_AGENT_ADMIN = "true";
23420
23421
  }
23422
+ if (operatorUid !== undefined) {
23423
+ env2.SWITCHROOM_WEBHOOK_RECEIVER_UID = String(operatorUid);
23424
+ }
23421
23425
  for (const [k, v] of Object.entries(a.userEnv)) {
23422
23426
  if (env2[k] === undefined)
23423
23427
  env2[k] = v;
@@ -49404,8 +49408,8 @@ var {
49404
49408
  } = import__.default;
49405
49409
 
49406
49410
  // src/build-info.ts
49407
- var VERSION = "0.14.9";
49408
- var COMMIT_SHA = "fd8ed8ed";
49411
+ var VERSION = "0.14.10";
49412
+ var COMMIT_SHA = "0cd391b5";
49409
49413
 
49410
49414
  // src/cli/agent.ts
49411
49415
  init_source();
@@ -71404,6 +71408,50 @@ function escapeHtml3(s) {
71404
71408
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
71405
71409
  }
71406
71410
 
71411
+ // src/web/webhook-ingest-client.ts
71412
+ import net4 from "node:net";
71413
+ function forwardToGateway(socketPath, req, opts = {}) {
71414
+ const timeoutMs = opts.timeoutMs ?? 5000;
71415
+ return new Promise((resolve28) => {
71416
+ let settled = false;
71417
+ let buf = "";
71418
+ const done = (value) => {
71419
+ if (settled)
71420
+ return;
71421
+ settled = true;
71422
+ try {
71423
+ conn.destroy();
71424
+ } catch {}
71425
+ resolve28(value);
71426
+ };
71427
+ const conn = net4.createConnection(socketPath);
71428
+ conn.setEncoding("utf8");
71429
+ conn.setTimeout(timeoutMs);
71430
+ conn.on("connect", () => {
71431
+ conn.write(JSON.stringify(req) + `
71432
+ `);
71433
+ });
71434
+ conn.on("data", (chunk) => {
71435
+ buf += chunk;
71436
+ const nl = buf.indexOf(`
71437
+ `);
71438
+ if (nl === -1)
71439
+ return;
71440
+ const line = buf.slice(0, nl);
71441
+ try {
71442
+ done(JSON.parse(line));
71443
+ } catch {
71444
+ done({ status: "error", error: "unparseable gateway response" });
71445
+ }
71446
+ });
71447
+ conn.on("timeout", () => done(null));
71448
+ conn.on("error", () => done(null));
71449
+ conn.on("close", () => {
71450
+ done(null);
71451
+ });
71452
+ });
71453
+ }
71454
+
71407
71455
  // src/web/webhook-handler.ts
71408
71456
  var KNOWN_SOURCES = ["github", "generic"];
71409
71457
  function jsonReply(status, body, extraHeaders) {
@@ -71559,7 +71607,7 @@ async function handleWebhookIngest(args, deps = {}) {
71559
71607
  `);
71560
71608
  return jsonReply(401, { ok: false, error: "unauthorized" });
71561
71609
  }
71562
- if (source === "github") {
71610
+ if (source === "github" && !args.viaGateway) {
71563
71611
  const deliveryId = args.headers.get("x-github-delivery");
71564
71612
  if (deliveryId) {
71565
71613
  const originalTs = dedupStore.check(args.agent, deliveryId, now);
@@ -71573,10 +71621,12 @@ async function handleWebhookIngest(args, deps = {}) {
71573
71621
  const rpm = args.config.rateLimit?.rpm;
71574
71622
  const retryAfter = rpm !== undefined ? rateLimiter.check(args.agent, source, rpm, now) : null;
71575
71623
  if (retryAfter !== null) {
71576
- const agentDir2 = resolveAgentDir(args.agent);
71577
- const telegramDir2 = join39(agentDir2, "telegram");
71578
- if (shouldWriteThrottleIssue(args.agent, source, now)) {
71579
- writeThrottleIssue(args.agent, source, now, telegramDir2, log);
71624
+ if (!args.viaGateway) {
71625
+ const agentDir2 = resolveAgentDir(args.agent);
71626
+ const telegramDir2 = join39(agentDir2, "telegram");
71627
+ if (shouldWriteThrottleIssue(args.agent, source, now)) {
71628
+ writeThrottleIssue(args.agent, source, now, telegramDir2, log);
71629
+ }
71580
71630
  }
71581
71631
  log(`webhook-ingest: agent='${args.agent}' source='${source}' rate limited retry-after=${retryAfter}s
71582
71632
  `);
@@ -71592,6 +71642,45 @@ async function handleWebhookIngest(args, deps = {}) {
71592
71642
  }
71593
71643
  const eventType = source === "github" ? args.headers.get("x-github-event") ?? "unknown" : args.source;
71594
71644
  const rendered = source === "github" ? renderGithubEvent(eventType, payload) : renderGenericEvent(args.source, payload);
71645
+ if (args.viaGateway) {
71646
+ const socketPath = join39(resolveAgentDir(args.agent), "telegram", "webhook.sock");
71647
+ const forward = deps.forwardFn ?? forwardToGateway;
71648
+ const deliveryId = source === "github" ? args.headers.get("x-github-delivery") ?? undefined : undefined;
71649
+ let resp;
71650
+ try {
71651
+ resp = await forward(socketPath, {
71652
+ agent: args.agent,
71653
+ source,
71654
+ event_type: eventType,
71655
+ ts: now,
71656
+ rendered_text: rendered.text,
71657
+ payload,
71658
+ ...deliveryId ? { delivery_id: deliveryId } : {}
71659
+ });
71660
+ } catch (err) {
71661
+ log(`webhook-ingest: agent='${args.agent}' source='${source}' gateway forward threw: ${err.message}
71662
+ `);
71663
+ resp = null;
71664
+ }
71665
+ if (resp === null) {
71666
+ log(`webhook-ingest: agent='${args.agent}' source='${source}' gateway unreachable
71667
+ `);
71668
+ return jsonReply(503, { ok: false, error: "gateway unavailable" });
71669
+ }
71670
+ if (resp.status === "error") {
71671
+ log(`webhook-ingest: agent='${args.agent}' source='${source}' gateway error: ${resp.error ?? "unknown"}
71672
+ `);
71673
+ return jsonReply(500, { ok: false, error: "gateway record failed" });
71674
+ }
71675
+ if (resp.status === "deduped") {
71676
+ log(`webhook-ingest: agent='${args.agent}' source='${source}' deduped (gateway) ts=${resp.ts}
71677
+ `);
71678
+ return jsonReply(200, { ok: true, deduped: true, ts: resp.ts });
71679
+ }
71680
+ log(`webhook-ingest: agent='${args.agent}' source='${source}' event='${eventType}' recorded via gateway ts=${resp.ts}
71681
+ `);
71682
+ return jsonReply(202, { ok: true, recorded: true, ts: resp.ts });
71683
+ }
71595
71684
  const agentDir = resolveAgentDir(args.agent);
71596
71685
  const telegramDir = join39(agentDir, "telegram");
71597
71686
  const logPath = join39(telegramDir, "webhook-events.jsonl");
@@ -71768,7 +71857,8 @@ async function handleWebhookRoute(req, agent, source, config) {
71768
71857
  allowedSources,
71769
71858
  config: { secrets: agentSecrets },
71770
71859
  agentExists: agentConfig !== undefined,
71771
- dispatchConfig: agentConfig?.channels?.telegram?.webhook_dispatch
71860
+ dispatchConfig: agentConfig?.channels?.telegram?.webhook_dispatch,
71861
+ viaGateway: agentConfig?.channels?.telegram?.webhook_via_gateway === true
71772
71862
  }, {});
71773
71863
  return new Response(result.body, {
71774
71864
  status: result.status,
@@ -13826,6 +13826,7 @@ var TelegramChannelSchema = exports_external.object({
13826
13826
  webhook_rate_limit: exports_external.object({
13827
13827
  rpm: exports_external.number().int().positive()
13828
13828
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
13829
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
13829
13830
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
13830
13831
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
13831
13832
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
@@ -11406,6 +11406,7 @@ var init_schema = __esm(() => {
11406
11406
  webhook_rate_limit: exports_external.object({
11407
11407
  rpm: exports_external.number().int().positive()
11408
11408
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11409
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11409
11410
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11410
11411
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11411
11412
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
@@ -11406,6 +11406,7 @@ var init_schema = __esm(() => {
11406
11406
  webhook_rate_limit: exports_external.object({
11407
11407
  rpm: exports_external.number().int().positive()
11408
11408
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11409
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11409
11410
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11410
11411
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Required when `chat_id` is set. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11411
11412
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.9",
3
+ "version": "0.14.10",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,9 +23,9 @@
23
23
  "build": "node scripts/build.mjs",
24
24
  "build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
25
25
  "pretest": "npm run build",
26
- "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts",
26
+ "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
27
27
  "test:vitest": "vitest run",
28
- "test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts",
28
+ "test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
29
29
  "test:watch": "vitest",
30
30
  "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs",
31
31
  "lint:tsc": "tsc --noEmit",