switchroom 0.15.11 → 0.15.13

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.
@@ -23993,6 +23993,12 @@ var init_schema = __esm(() => {
23993
23993
  }).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."),
23994
23994
  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."),
23995
23995
  webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
23996
+ linear_agent: exports_external.object({
23997
+ enabled: exports_external.boolean(),
23998
+ token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
23999
+ workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
24000
+ default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
24001
+ }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
23996
24002
  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."),
23997
24003
  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. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "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`."),
23998
24004
  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.")
@@ -24875,6 +24881,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
24875
24881
  }
24876
24882
  merged.reaction_dispatch = combined;
24877
24883
  }
24884
+ const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
24885
+ if (linearEnabled) {
24886
+ const rd = merged.reaction_dispatch;
24887
+ if (!rd || rd.emojis === undefined) {
24888
+ merged.reaction_dispatch = {
24889
+ enabled: rd?.enabled ?? true,
24890
+ emojis: ["\uD83D\uDC68\u200d\uD83D\uDCBB", "\uD83D\uDCCC"]
24891
+ };
24892
+ }
24893
+ }
24878
24894
  if (defaults.resources || merged.resources) {
24879
24895
  const d = defaults.resources ?? {};
24880
24896
  const a = merged.resources ?? {};
@@ -31450,7 +31466,7 @@ import {
31450
31466
  appendFileSync as appendFileSync5
31451
31467
  } from "fs";
31452
31468
  import { homedir as homedir14 } from "os";
31453
- import { join as join35, extname, sep as sep3, basename as basename7 } from "path";
31469
+ import { join as join35, extname, sep as sep3, basename as basename9 } from "path";
31454
31470
 
31455
31471
  // plugin-logger.ts
31456
31472
  import { appendFileSync, mkdirSync, renameSync, statSync, existsSync } from "fs";
@@ -44920,7 +44936,7 @@ function labelTag(label) {
44920
44936
  }
44921
44937
 
44922
44938
  // gateway/model-command.ts
44923
- var MODEL_ALIASES = ["opus", "sonnet", "haiku", "default"];
44939
+ var MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
44924
44940
  var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,99}$/;
44925
44941
  function isValidModelArg(arg) {
44926
44942
  return MODEL_ARG_RE.test(arg);
@@ -45821,6 +45837,16 @@ function mergeAgentConfig2(defaultsIn, agentIn) {
45821
45837
  }
45822
45838
  merged.reaction_dispatch = combined;
45823
45839
  }
45840
+ const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
45841
+ if (linearEnabled) {
45842
+ const rd = merged.reaction_dispatch;
45843
+ if (!rd || rd.emojis === undefined) {
45844
+ merged.reaction_dispatch = {
45845
+ enabled: rd?.enabled ?? true,
45846
+ emojis: ["\uD83D\uDC68\u200d\uD83D\uDCBB", "\uD83D\uDCCC"]
45847
+ };
45848
+ }
45849
+ }
45824
45850
  if (defaults.resources || merged.resources) {
45825
45851
  const d = defaults.resources ?? {};
45826
45852
  const a = merged.resources ?? {};
@@ -47026,6 +47052,53 @@ function injectWebhookInbound(agent, prompt, ctx, deps = {}) {
47026
47052
  `)).catch((err) => log(`webhook-dispatch: agent='${agent}' inject failed: ${String(err)}
47027
47053
  `));
47028
47054
  }
47055
+ function parseLinearAgentSession(payload) {
47056
+ const type = String(payload.type ?? "").toLowerCase();
47057
+ if (type !== "agentsessionevent")
47058
+ return null;
47059
+ const action = String(payload.action ?? "").toLowerCase();
47060
+ const session = payload.agentSession ?? payload.agent_session ?? {};
47061
+ const sessionId = String(session.id ?? payload.agentSessionId ?? payload.agent_session_id ?? "");
47062
+ if (!sessionId)
47063
+ return null;
47064
+ const explicitTrigger = String(session.trigger ?? payload.trigger ?? "").toLowerCase();
47065
+ const comment = session.comment;
47066
+ const trigger = explicitTrigger === "mention" || explicitTrigger === "delegation" ? explicitTrigger : comment ? "mention" : "delegation";
47067
+ const promptContext = typeof payload.promptContext === "string" ? payload.promptContext : typeof session.promptContext === "string" ? session.promptContext : undefined;
47068
+ const issue = session.issue ?? {};
47069
+ const issueId = String(issue.identifier ?? "");
47070
+ const issueTitle = String(issue.title ?? "");
47071
+ const commentBody = typeof comment?.body === "string" ? comment.body : "";
47072
+ const summaryParts = [];
47073
+ summaryParts.push(trigger === "mention" ? `You were @mentioned in Linear` : `An issue was delegated to you in Linear`);
47074
+ if (issueId || issueTitle) {
47075
+ summaryParts.push(`Issue ${issueId}${issueTitle ? `: ${issueTitle}` : ""}`.trim());
47076
+ }
47077
+ if (commentBody)
47078
+ summaryParts.push(commentBody);
47079
+ const summary = summaryParts.join(`
47080
+ `);
47081
+ return { sessionId, trigger, action, promptContext, summary };
47082
+ }
47083
+ function buildLinearInbound(session, ctx, now) {
47084
+ const content = session.promptContext && session.promptContext.trim().length > 0 ? session.promptContext : session.summary;
47085
+ return {
47086
+ type: "inbound",
47087
+ chatId: ctx.chatId,
47088
+ ...ctx.threadId !== undefined ? { threadId: ctx.threadId } : {},
47089
+ messageId: now,
47090
+ user: "linear",
47091
+ userId: 0,
47092
+ ts: now,
47093
+ text: content,
47094
+ meta: {
47095
+ source: "linear",
47096
+ agent_session_id: session.sessionId,
47097
+ linear_trigger: session.trigger,
47098
+ linear_event: `agent_session.${session.action || "event"}`
47099
+ }
47100
+ };
47101
+ }
47029
47102
  function evaluateDispatch(args, deps = {}) {
47030
47103
  const log = deps.log ?? ((s) => process.stderr.write(s));
47031
47104
  const now = (deps.now ?? Date.now)();
@@ -47137,6 +47210,38 @@ function recordWebhookEvent(rec, deps = {}) {
47137
47210
  }
47138
47211
  log(`webhook-gateway: agent='${agent}' source='${rec.source}' event='${rec.event_type}' recorded ts=${now}
47139
47212
  `);
47213
+ if (rec.source === "linear") {
47214
+ try {
47215
+ const session = parseLinearAgentSession(rec.payload);
47216
+ if (session) {
47217
+ const config = (deps.loadConfig ?? loadConfig)();
47218
+ const rawAgent = config.agents?.[agent];
47219
+ const linearAgent = rawAgent ? resolveAgentConfig(config.defaults, config.profiles, rawAgent).channels?.telegram?.linear_agent : undefined;
47220
+ if (!linearAgent?.enabled) {
47221
+ log(`linear-agent: agent='${agent}' session='${session.sessionId}' ignored \u2014 linear_agent not enabled
47222
+ `);
47223
+ } else {
47224
+ const target = resolveChannelTarget(config, agent);
47225
+ if (!target) {
47226
+ log(`linear-agent: agent='${agent}' session='${session.sessionId}' skipped \u2014 no chat target (forum_chat_id / chat_id unset)
47227
+ `);
47228
+ } else {
47229
+ const inbound = buildLinearInbound(session, {
47230
+ chatId: target.chatId,
47231
+ ...target.threadId !== undefined ? { threadId: target.threadId } : {}
47232
+ }, now);
47233
+ const ok = deps.inject ? deps.inject(agent, inbound) : false;
47234
+ log(`linear-agent: agent='${agent}' session='${session.sessionId}' trigger='${session.trigger}' ` + `${ok ? "injected" : "NOT injected (no inject sink)"}
47235
+ `);
47236
+ return { status: "ok", ts: now, dispatched: ok ? 1 : 0 };
47237
+ }
47238
+ }
47239
+ }
47240
+ } catch (err) {
47241
+ log(`linear-agent: agent='${agent}' session inject error (event recorded): ${err.message}
47242
+ `);
47243
+ }
47244
+ }
47140
47245
  let dispatched = 0;
47141
47246
  try {
47142
47247
  const config = (deps.loadConfig ?? loadConfig)();
@@ -52697,9 +52802,10 @@ function defaultReadEvents(stateDir) {
52697
52802
  return readAll(stateDir);
52698
52803
  }
52699
52804
  // permission-title.ts
52700
- import { basename as basename5 } from "node:path";
52805
+ import { basename as basename6 } from "node:path";
52701
52806
 
52702
52807
  // permission-rule.ts
52808
+ import { basename as basename5 } from "node:path";
52703
52809
  var FILE_TOOLS = new Set([
52704
52810
  "Edit",
52705
52811
  "Write",
@@ -52720,6 +52826,83 @@ var BROAD_ONLY_TOOLS = new Set([
52720
52826
  function prettyMcpServer(server) {
52721
52827
  return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
52722
52828
  }
52829
+ function resolveSkillName(input) {
52830
+ return readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
52831
+ }
52832
+ function filePathFrom(input) {
52833
+ if (!input)
52834
+ return null;
52835
+ return readString(input, "file_path") ?? readString(input, "notebook_path");
52836
+ }
52837
+ function bashFirstToken(command) {
52838
+ const m = /^\s*([^\s|&;<>()`$]+)/.exec(command);
52839
+ if (!m)
52840
+ return null;
52841
+ const tok = m[1];
52842
+ if (tok.includes(".."))
52843
+ return null;
52844
+ return /^[A-Za-z0-9._\-\/]+$/.test(tok) ? tok : null;
52845
+ }
52846
+ function parseInput(raw) {
52847
+ if (!raw || typeof raw !== "string")
52848
+ return null;
52849
+ const trimmed = raw.trim();
52850
+ if (!trimmed.startsWith("{"))
52851
+ return null;
52852
+ try {
52853
+ const parsed = JSON.parse(trimmed);
52854
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
52855
+ return parsed;
52856
+ }
52857
+ } catch {}
52858
+ return null;
52859
+ }
52860
+ function readString(input, key) {
52861
+ const value = input[key];
52862
+ return typeof value === "string" && value.length > 0 ? value : null;
52863
+ }
52864
+ function skillBasenameFromPath(input) {
52865
+ const path = readString(input, "path") ?? readString(input, "skill_path");
52866
+ if (!path)
52867
+ return null;
52868
+ const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
52869
+ return basename5(trimmed) || null;
52870
+ }
52871
+ function matchesAllowRule(rule, toolName, inputPreview) {
52872
+ if (!rule || !toolName)
52873
+ return false;
52874
+ if (rule.endsWith("__*") && rule.startsWith("mcp__")) {
52875
+ const prefix = rule.slice(0, -1);
52876
+ return toolName.startsWith(prefix);
52877
+ }
52878
+ const scoped = /^([A-Za-z]+)\((.+)\)$/.exec(rule);
52879
+ if (scoped) {
52880
+ const ruleTool = scoped[1];
52881
+ const arg = scoped[2];
52882
+ if (ruleTool !== toolName)
52883
+ return false;
52884
+ const input = parseInput(inputPreview);
52885
+ if (ruleTool === "Skill") {
52886
+ if (!input)
52887
+ return false;
52888
+ return resolveSkillName(input) === arg;
52889
+ }
52890
+ if (ruleTool === "Bash") {
52891
+ const cmd = input ? readString(input, "command") : null;
52892
+ if (!cmd)
52893
+ return false;
52894
+ const m = /^([^:]+):\*$/.exec(arg);
52895
+ if (!m)
52896
+ return false;
52897
+ return bashFirstToken(cmd) === m[1];
52898
+ }
52899
+ if (FILE_TOOLS.has(ruleTool)) {
52900
+ return filePathFrom(input) === arg;
52901
+ }
52902
+ return false;
52903
+ }
52904
+ return rule === toolName;
52905
+ }
52723
52906
 
52724
52907
  // permission-title.ts
52725
52908
  init_redact();
@@ -52778,7 +52961,7 @@ function formatPermissionCardBody(opts) {
52778
52961
  `);
52779
52962
  }
52780
52963
  function naturalAction(toolName, inputPreview) {
52781
- const input = parseInput(inputPreview);
52964
+ const input = parseInput2(inputPreview);
52782
52965
  if (toolName.startsWith("mcp__"))
52783
52966
  return naturalMcpAction(toolName, input);
52784
52967
  switch (toolName) {
@@ -52797,24 +52980,24 @@ function naturalAction(toolName, inputPreview) {
52797
52980
  return f ? `read: ${f}` : "read files";
52798
52981
  }
52799
52982
  case "Bash": {
52800
- const c = input ? readString(input, "command") : null;
52983
+ const c = input ? readString2(input, "command") : null;
52801
52984
  return c ? `run: ${truncate6(c, COMMAND_TITLE_MAX)}` : "run shell commands";
52802
52985
  }
52803
52986
  case "Skill": {
52804
- const s = input ? resolveSkillName(input) : null;
52987
+ const s = input ? resolveSkillName2(input) : null;
52805
52988
  return s ? `use the ${s} skill` : "use a skill";
52806
52989
  }
52807
52990
  case "Glob":
52808
52991
  case "Grep": {
52809
- const p = input ? readString(input, "pattern") : null;
52992
+ const p = input ? readString2(input, "pattern") : null;
52810
52993
  return p ? `search files for: ${truncate6(p, COMMAND_TITLE_MAX)}` : "search files";
52811
52994
  }
52812
52995
  case "WebSearch": {
52813
- const q = input ? readString(input, "query") : null;
52996
+ const q = input ? readString2(input, "query") : null;
52814
52997
  return q ? `search the web for: ${truncate6(q, COMMAND_TITLE_MAX)}` : "search the web";
52815
52998
  }
52816
52999
  case "WebFetch": {
52817
- const u = input ? readString(input, "url") : null;
53000
+ const u = input ? readString2(input, "url") : null;
52818
53001
  return u ? `fetch a web page: ${truncate6(u, COMMAND_TITLE_MAX)}` : "fetch a web page";
52819
53002
  }
52820
53003
  case "Task":
@@ -52852,7 +53035,7 @@ function restResourcePhrase(server, verb, input) {
52852
53035
  return null;
52853
53036
  let path = null;
52854
53037
  for (const key of RESOURCE_KEYS) {
52855
- path = readString(input, key);
53038
+ path = readString2(input, key);
52856
53039
  if (path)
52857
53040
  break;
52858
53041
  }
@@ -52868,7 +53051,7 @@ function mcpArgSummary(toolName, inputPreview) {
52868
53051
  const server = toolName.split("__")[1] ?? "";
52869
53052
  if (INTERNAL_MCP_SERVERS.has(server))
52870
53053
  return null;
52871
- const input = parseInput(inputPreview);
53054
+ const input = parseInput2(inputPreview);
52872
53055
  if (!input)
52873
53056
  return null;
52874
53057
  const payload = input.body ?? input.query;
@@ -52912,11 +53095,11 @@ function describeGrant(toolName, inputPreview, option) {
52912
53095
  return m ? `run ${m[1]} commands` : "run that command";
52913
53096
  }
52914
53097
  if (t === "Edit" || t === "MultiEdit" || t === "NotebookEdit")
52915
- return `edit ${basename5(arg)}`;
53098
+ return `edit ${basename6(arg)}`;
52916
53099
  if (t === "Write")
52917
- return `write ${basename5(arg)}`;
53100
+ return `write ${basename6(arg)}`;
52918
53101
  if (t === "Read")
52919
- return `read ${basename5(arg)}`;
53102
+ return `read ${basename6(arg)}`;
52920
53103
  return naturalAction(toolName, inputPreview);
52921
53104
  }
52922
53105
  switch (rule) {
@@ -52948,14 +53131,14 @@ function formatPermissionResumeMessage(opts) {
52948
53131
  }
52949
53132
  return hasAction ? `\uD83D\uDEAB ${who} \u2014 noted, I won't ${escapeTgHtml(lowerFirst(act))}. Continuing without it.` : `\uD83D\uDEAB ${who} \u2014 noted, continuing without it.`;
52950
53133
  }
52951
- function resolveSkillName(input) {
52952
- return readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
53134
+ function resolveSkillName2(input) {
53135
+ return readString2(input, "skill") ?? readString2(input, "skill_name") ?? readString2(input, "skillName") ?? readString2(input, "name") ?? skillBasenameFromPath2(input);
52953
53136
  }
52954
53137
  function fileBase(input) {
52955
53138
  if (!input)
52956
53139
  return null;
52957
- const p = readString(input, "file_path") ?? readString(input, "notebook_path");
52958
- return p ? basename5(p) : null;
53140
+ const p = readString2(input, "file_path") ?? readString2(input, "notebook_path");
53141
+ return p ? basename6(p) : null;
52959
53142
  }
52960
53143
  function lowerFirst(text) {
52961
53144
  return text.length > 0 ? text.charAt(0).toLowerCase() + text.slice(1) : text;
@@ -52966,7 +53149,7 @@ function capFirst(text) {
52966
53149
  function escapeTgHtml(text) {
52967
53150
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
52968
53151
  }
52969
- function parseInput(raw) {
53152
+ function parseInput2(raw) {
52970
53153
  if (!raw || typeof raw !== "string")
52971
53154
  return null;
52972
53155
  const trimmed = raw.trim();
@@ -52980,12 +53163,12 @@ function parseInput(raw) {
52980
53163
  } catch {}
52981
53164
  return null;
52982
53165
  }
52983
- function readString(input, key) {
53166
+ function readString2(input, key) {
52984
53167
  const value = input[key];
52985
53168
  return typeof value === "string" && value.length > 0 ? value : null;
52986
53169
  }
52987
- function skillBasenameFromPath(input) {
52988
- const path = readString(input, "path") ?? readString(input, "skill_path");
53170
+ function skillBasenameFromPath2(input) {
53171
+ const path = readString2(input, "path") ?? readString2(input, "skill_path");
52989
53172
  if (!path)
52990
53173
  return null;
52991
53174
  const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
@@ -53001,7 +53184,7 @@ function truncate6(text, max) {
53001
53184
  }
53002
53185
 
53003
53186
  // permission-rule.ts
53004
- import { basename as basename6 } from "node:path";
53187
+ import { basename as basename7 } from "node:path";
53005
53188
  var FILE_TOOLS2 = new Set([
53006
53189
  "Edit",
53007
53190
  "Write",
@@ -53022,9 +53205,9 @@ var BROAD_ONLY_TOOLS2 = new Set([
53022
53205
  function resolveScopedAllowChoices(toolName, inputPreview) {
53023
53206
  if (!toolName)
53024
53207
  return null;
53025
- const input = parseInput2(inputPreview);
53208
+ const input = parseInput3(inputPreview);
53026
53209
  if (toolName === "Skill") {
53027
- const skill = input ? resolveSkillName2(input) : null;
53210
+ const skill = input ? resolveSkillName3(input) : null;
53028
53211
  if (!skill)
53029
53212
  return null;
53030
53213
  if (!/^[A-Za-z0-9._\-+]+$/.test(skill))
@@ -53035,7 +53218,7 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
53035
53218
  };
53036
53219
  }
53037
53220
  if (FILE_TOOLS2.has(toolName)) {
53038
- const path = filePathFrom(input);
53221
+ const path = filePathFrom2(input);
53039
53222
  const broad = { rule: toolName, buttonLabel: "Any file", broad: true };
53040
53223
  if (path) {
53041
53224
  return {
@@ -53047,8 +53230,8 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
53047
53230
  }
53048
53231
  if (toolName === "Bash") {
53049
53232
  const broad = { rule: "Bash", buttonLabel: "Any command", broad: true };
53050
- const cmd = input ? readString2(input, "command") : null;
53051
- const tok = cmd ? bashFirstToken(cmd) : null;
53233
+ const cmd = input ? readString3(input, "command") : null;
53234
+ const tok = cmd ? bashFirstToken2(cmd) : null;
53052
53235
  if (tok) {
53053
53236
  return {
53054
53237
  specific: { rule: `Bash(${tok}:*)`, buttonLabel: `${tok} commands`, broad: false },
@@ -53076,15 +53259,15 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
53076
53259
  function prettyMcpServer2(server) {
53077
53260
  return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
53078
53261
  }
53079
- function resolveSkillName2(input) {
53080
- return readString2(input, "skill") ?? readString2(input, "skill_name") ?? readString2(input, "skillName") ?? readString2(input, "name") ?? skillBasenameFromPath2(input);
53262
+ function resolveSkillName3(input) {
53263
+ return readString3(input, "skill") ?? readString3(input, "skill_name") ?? readString3(input, "skillName") ?? readString3(input, "name") ?? skillBasenameFromPath3(input);
53081
53264
  }
53082
- function filePathFrom(input) {
53265
+ function filePathFrom2(input) {
53083
53266
  if (!input)
53084
53267
  return null;
53085
- return readString2(input, "file_path") ?? readString2(input, "notebook_path");
53268
+ return readString3(input, "file_path") ?? readString3(input, "notebook_path");
53086
53269
  }
53087
- function bashFirstToken(command) {
53270
+ function bashFirstToken2(command) {
53088
53271
  const m = /^\s*([^\s|&;<>()`$]+)/.exec(command);
53089
53272
  if (!m)
53090
53273
  return null;
@@ -53093,7 +53276,7 @@ function bashFirstToken(command) {
53093
53276
  return null;
53094
53277
  return /^[A-Za-z0-9._\-\/]+$/.test(tok) ? tok : null;
53095
53278
  }
53096
- function parseInput2(raw) {
53279
+ function parseInput3(raw) {
53097
53280
  if (!raw || typeof raw !== "string")
53098
53281
  return null;
53099
53282
  const trimmed = raw.trim();
@@ -53107,21 +53290,136 @@ function parseInput2(raw) {
53107
53290
  } catch {}
53108
53291
  return null;
53109
53292
  }
53110
- function readString2(input, key) {
53293
+ function readString3(input, key) {
53111
53294
  const value = input[key];
53112
53295
  return typeof value === "string" && value.length > 0 ? value : null;
53113
53296
  }
53114
- function skillBasenameFromPath2(input) {
53115
- const path = readString2(input, "path") ?? readString2(input, "skill_path");
53297
+ function skillBasenameFromPath3(input) {
53298
+ const path = readString3(input, "path") ?? readString3(input, "skill_path");
53116
53299
  if (!path)
53117
53300
  return null;
53118
53301
  const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
53119
- return basename6(trimmed) || null;
53302
+ return basename7(trimmed) || null;
53120
53303
  }
53121
53304
  function isRulePersisted(resolvedAllow, ruleRule) {
53122
53305
  return resolvedAllow.includes(ruleRule);
53123
53306
  }
53124
53307
 
53308
+ // scoped-approval.ts
53309
+ import { basename as basename8 } from "node:path";
53310
+ var SCOPED_APPROVAL_DEFAULT_TTL_MS = 30 * 60 * 1000;
53311
+ function scopedApprovalTtlMs(env = process.env) {
53312
+ const raw = env.SWITCHROOM_SCOPED_APPROVAL_TTL_MS;
53313
+ if (raw === undefined || raw.trim() === "")
53314
+ return SCOPED_APPROVAL_DEFAULT_TTL_MS;
53315
+ const n = Number(raw);
53316
+ if (!Number.isFinite(n) || n < 0)
53317
+ return SCOPED_APPROVAL_DEFAULT_TTL_MS;
53318
+ return Math.floor(n);
53319
+ }
53320
+ var FILE_RULE = /^(Edit|Write|MultiEdit|NotebookEdit|Read)\((.+)\)$/;
53321
+ var BASH_FAMILY_RULE = /^Bash\(([^:]+):\*\)$/;
53322
+ function resolveTimeBox(toolName, inputPreview, choices) {
53323
+ const specific = choices?.specific;
53324
+ if (!specific || specific.broad)
53325
+ return null;
53326
+ const rule = specific.rule;
53327
+ const fileMatch = FILE_RULE.exec(rule);
53328
+ if (fileMatch) {
53329
+ const verb = fileMatch[1] === "Read" ? "reads of" : "edits to";
53330
+ return { rule, breadth: `${verb} ${basename8(fileMatch[2])}` };
53331
+ }
53332
+ const bashMatch = BASH_FAMILY_RULE.exec(rule);
53333
+ if (bashMatch) {
53334
+ const cmd = readBashCommand(inputPreview);
53335
+ if (!cmd || isDestructiveBashCommand(cmd))
53336
+ return null;
53337
+ return { rule, breadth: `any \`${bashMatch[1]}\` command` };
53338
+ }
53339
+ return null;
53340
+ }
53341
+ function recordScopedGrant(store2, agent, rule, now, ttlMs) {
53342
+ if (ttlMs <= 0)
53343
+ return;
53344
+ const list2 = store2.get(agent) ?? [];
53345
+ const others = list2.filter((g) => g.rule !== rule);
53346
+ others.push({ rule, expiresAt: now + ttlMs });
53347
+ store2.set(agent, others);
53348
+ }
53349
+ function lookupScopedGrant(store2, agent, toolName, inputPreview, now) {
53350
+ const list2 = store2.get(agent);
53351
+ if (!list2 || list2.length === 0)
53352
+ return null;
53353
+ for (const g of list2) {
53354
+ if (g.expiresAt <= now)
53355
+ continue;
53356
+ if (!matchesAllowRule(g.rule, toolName, inputPreview))
53357
+ continue;
53358
+ if (toolName === "Bash") {
53359
+ const cmd = readBashCommand(inputPreview);
53360
+ if (!cmd || isDestructiveBashCommand(cmd))
53361
+ return null;
53362
+ }
53363
+ return g.rule;
53364
+ }
53365
+ return null;
53366
+ }
53367
+ function sweepScopedGrants(store2, now) {
53368
+ for (const [agent, list2] of store2) {
53369
+ const live = list2.filter((g) => g.expiresAt > now);
53370
+ if (live.length === 0)
53371
+ store2.delete(agent);
53372
+ else if (live.length !== list2.length)
53373
+ store2.set(agent, live);
53374
+ }
53375
+ }
53376
+ function isDestructiveBashCommand(command) {
53377
+ if (!command || !command.trim())
53378
+ return true;
53379
+ const c = command.toLowerCase();
53380
+ if (c.includes("`"))
53381
+ return true;
53382
+ if (/\|\s*(sudo\s+)?(sh|bash|zsh|fish|python\d?|perl|ruby|node)\b/.test(c))
53383
+ return true;
53384
+ if (/(^|\s|;|&&|\|\||\()sudo\b/.test(c))
53385
+ return true;
53386
+ if (/(^|\s|;|&&|\|\||\()(su|doas)\s/.test(c))
53387
+ return true;
53388
+ if (/(^|\s|;|&&|\|\||\()(rm|rmdir|dd|shred|truncate|fdisk|mkfs\S*|wipefs|blkdiscard|fallocate)\b/.test(c))
53389
+ return true;
53390
+ if (/\b(chmod|chown|chgrp)\b[^|;&]*(\s-(-recursive|[a-z]*r[a-z]*)\b)/.test(c))
53391
+ return true;
53392
+ if (/>\s*\/(dev|etc|boot|sys|proc)\b/.test(c))
53393
+ return true;
53394
+ if (/\bgit\b/.test(c) && /(push\b[^|;&]*(--force|-f\b|--force-with-lease)|push\s+[^\s]*\s+\+|reset\s+--hard|clean\s+-[a-z]*[fd]|filter-branch|reflog\s+expire|update-ref\s+-d|branch\s+-d{1,2}\b|checkout\s+--\s|restore\b)/.test(c))
53395
+ return true;
53396
+ if (/(^|\s|;|&&|\|\||\()(shutdown|reboot|halt|poweroff|kill|killall|pkill)\b/.test(c))
53397
+ return true;
53398
+ if (/(^|\s)init\s+0\b/.test(c))
53399
+ return true;
53400
+ if (/\bdocker\b[^|;&]*\b(rm|prune)\b/.test(c))
53401
+ return true;
53402
+ if (/(^|\s)(apt|apt-get|yum|dnf|brew|pacman|npm|pnpm|yarn|pip\d?)\b[^|;&]*\b(remove|uninstall|purge|prune)\b/.test(c))
53403
+ return true;
53404
+ if (/:\s*\(\s*\)\s*\{/.test(c))
53405
+ return true;
53406
+ return false;
53407
+ }
53408
+ function readBashCommand(inputPreview) {
53409
+ if (!inputPreview || typeof inputPreview !== "string")
53410
+ return null;
53411
+ const trimmed = inputPreview.trim();
53412
+ if (!trimmed.startsWith("{"))
53413
+ return null;
53414
+ try {
53415
+ const parsed = JSON.parse(trimmed);
53416
+ const cmd = parsed?.command;
53417
+ return typeof cmd === "string" && cmd.length > 0 ? cmd : null;
53418
+ } catch {
53419
+ return null;
53420
+ }
53421
+ }
53422
+
53125
53423
  // permission-diff.ts
53126
53424
  var TARGET_HEADER_A = "--- a/switchroom.yaml";
53127
53425
  var TARGET_HEADER_B = "+++ b/switchroom.yaml";
@@ -53793,11 +54091,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
53793
54091
  }
53794
54092
 
53795
54093
  // ../src/build-info.ts
53796
- var VERSION = "0.15.11";
53797
- var COMMIT_SHA = "43331954";
53798
- var COMMIT_DATE = "2026-06-13T03:24:01Z";
53799
- var LATEST_PR = 2308;
53800
- var COMMITS_AHEAD_OF_TAG = 0;
54094
+ var VERSION = "0.15.13";
54095
+ var COMMIT_SHA = "36ba2682";
54096
+ var COMMIT_DATE = "2026-06-13T16:49:14+10:00";
54097
+ var LATEST_PR = null;
54098
+ var COMMITS_AHEAD_OF_TAG = 6;
53801
54099
 
53802
54100
  // gateway/boot-version.ts
53803
54101
  function formatRelativeAgo(iso) {
@@ -54087,6 +54385,210 @@ async function revokeGrantViaBroker(id, opts) {
54087
54385
  return { kind: "error", msg: "unexpected broker response" };
54088
54386
  }
54089
54387
 
54388
+ // gateway/linear-activity.ts
54389
+ init_client2();
54390
+ var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
54391
+ async function defaultResolveLinearToken(agent) {
54392
+ const key = `linear/${agent}/token`;
54393
+ const token = readVaultTokenFile(agent) ?? undefined;
54394
+ const result = await getViaBrokerStructured(key, token ? { token } : {});
54395
+ if (result.kind === "ok" && result.entry.kind === "string") {
54396
+ return { ok: true, token: result.entry.value };
54397
+ }
54398
+ if (result.kind === "unreachable")
54399
+ return { ok: false, reason: "unreachable" };
54400
+ if (result.kind === "not_found")
54401
+ return { ok: false, reason: "not_found" };
54402
+ if (result.kind === "denied")
54403
+ return { ok: false, reason: "denied" };
54404
+ return { ok: false, reason: "unknown" };
54405
+ }
54406
+ async function emitLinearAgentActivity(args, deps = {}) {
54407
+ const log = deps.log ?? ((s) => process.stderr.write(s));
54408
+ const sessionId = args.agent_session_id;
54409
+ if (!sessionId)
54410
+ throw new Error("linear_agent_activity: agent_session_id is required");
54411
+ const type = args.type;
54412
+ if (!type || !["thought", "message", "complete", "error"].includes(type)) {
54413
+ throw new Error("linear_agent_activity: type must be one of thought|message|complete|error");
54414
+ }
54415
+ const body = args.body;
54416
+ if (type !== "complete" && (body == null || body === "")) {
54417
+ throw new Error(`linear_agent_activity: body is required for type='${type}'`);
54418
+ }
54419
+ const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? "-";
54420
+ const resolveToken = deps.resolveToken ?? defaultResolveLinearToken;
54421
+ const tokenResult = await resolveToken(agent);
54422
+ if (!tokenResult.ok) {
54423
+ if (tokenResult.reason === "denied" || tokenResult.reason === "not_found") {
54424
+ return {
54425
+ content: [
54426
+ {
54427
+ type: "text",
54428
+ text: `linear_agent_activity failed: no Linear token (vault ${tokenResult.reason}). ` + `Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.`
54429
+ }
54430
+ ]
54431
+ };
54432
+ }
54433
+ return {
54434
+ content: [
54435
+ {
54436
+ type: "text",
54437
+ text: `linear_agent_activity failed: vault broker ${tokenResult.reason} resolving 'linear/${agent}/token'.`
54438
+ }
54439
+ ]
54440
+ };
54441
+ }
54442
+ const content = { type };
54443
+ if (body != null && body !== "")
54444
+ content.body = body;
54445
+ const mutation = "mutation AgentActivityCreate($input: AgentActivityCreateInput!) { " + "agentActivityCreate(input: $input) { success agentActivity { id } } }";
54446
+ const variables = { input: { agentSessionId: sessionId, content } };
54447
+ const fetchImpl = deps.fetchImpl ?? fetch;
54448
+ let resp;
54449
+ try {
54450
+ resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
54451
+ method: "POST",
54452
+ headers: {
54453
+ "Content-Type": "application/json",
54454
+ Authorization: tokenResult.token
54455
+ },
54456
+ body: JSON.stringify({ query: mutation, variables })
54457
+ });
54458
+ } catch (err) {
54459
+ return {
54460
+ content: [{ type: "text", text: `linear_agent_activity failed: request error: ${err.message}` }]
54461
+ };
54462
+ }
54463
+ if (!resp.ok) {
54464
+ const txt = await resp.text().catch(() => "");
54465
+ return {
54466
+ content: [
54467
+ { type: "text", text: `linear_agent_activity failed: Linear API ${resp.status}${txt ? ` \u2014 ${txt.slice(0, 200)}` : ""}` }
54468
+ ]
54469
+ };
54470
+ }
54471
+ let json;
54472
+ try {
54473
+ json = await resp.json();
54474
+ } catch {
54475
+ return { content: [{ type: "text", text: "linear_agent_activity failed: malformed Linear API response" }] };
54476
+ }
54477
+ if (json.errors && json.errors.length > 0) {
54478
+ return {
54479
+ content: [
54480
+ { type: "text", text: `linear_agent_activity failed: ${json.errors.map((e) => e.message ?? "error").join("; ").slice(0, 300)}` }
54481
+ ]
54482
+ };
54483
+ }
54484
+ if (json.data?.agentActivityCreate?.success === false) {
54485
+ return { content: [{ type: "text", text: "linear_agent_activity failed: Linear reported success=false" }] };
54486
+ }
54487
+ log(`telegram gateway: linear_agent_activity: emitted type=${type} session=${sessionId} agent=${agent}
54488
+ `);
54489
+ return { content: [{ type: "text", text: `Linear ${type} emitted on session ${sessionId}` }] };
54490
+ }
54491
+ function captureDedupMarker(dedupKey) {
54492
+ return `
54493
+
54494
+ <!-- switchroom-capture: ${dedupKey} -->`;
54495
+ }
54496
+ async function createLinearIssue(args, deps = {}) {
54497
+ const log = deps.log ?? ((s) => process.stderr.write(s));
54498
+ const fetchImpl = deps.fetchImpl ?? fetch;
54499
+ const title = args.title;
54500
+ if (!title || title.trim() === "")
54501
+ throw new Error("linear_create_issue: title is required");
54502
+ const body = args.body ?? "";
54503
+ const teamIdArg = args.team_id ?? (deps.defaultTeamId ?? process.env.SWITCHROOM_LINEAR_DEFAULT_TEAM_ID) ?? undefined;
54504
+ const dedupKey = args.dedup_key ?? undefined;
54505
+ const priority = typeof args.priority === "number" ? args.priority : undefined;
54506
+ const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? "-";
54507
+ const resolveToken = deps.resolveToken ?? defaultResolveLinearToken;
54508
+ const tokenResult = await resolveToken(agent);
54509
+ if (!tokenResult.ok) {
54510
+ const hint = tokenResult.reason === "denied" || tokenResult.reason === "not_found" ? ` Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.` : "";
54511
+ return {
54512
+ content: [
54513
+ { type: "text", text: `Couldn't file to Linear: no token (vault ${tokenResult.reason}).${hint}` }
54514
+ ]
54515
+ };
54516
+ }
54517
+ const token = tokenResult.token;
54518
+ const gql = async (query2, variables) => {
54519
+ let resp;
54520
+ try {
54521
+ resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
54522
+ method: "POST",
54523
+ headers: { "Content-Type": "application/json", Authorization: token },
54524
+ body: JSON.stringify({ query: query2, variables })
54525
+ });
54526
+ } catch (err) {
54527
+ return { ok: false, text: `request error: ${err.message}` };
54528
+ }
54529
+ if (!resp.ok) {
54530
+ const txt = await resp.text().catch(() => "");
54531
+ return { ok: false, text: `Linear API ${resp.status}${txt ? ` \u2014 ${txt.slice(0, 200)}` : ""}` };
54532
+ }
54533
+ let json;
54534
+ try {
54535
+ json = await resp.json();
54536
+ } catch {
54537
+ return { ok: false, text: "malformed Linear API response" };
54538
+ }
54539
+ if (json.errors && json.errors.length > 0) {
54540
+ return { ok: false, text: json.errors.map((e) => e.message ?? "error").join("; ").slice(0, 300) };
54541
+ }
54542
+ return { ok: true, data: json.data };
54543
+ };
54544
+ if (dedupKey) {
54545
+ const search = await gql("query($term: String!) { searchIssues(term: $term) { nodes { id url title } } }", { term: dedupKey });
54546
+ if (search.ok) {
54547
+ const hit = (search.data?.searchIssues?.nodes ?? [])[0];
54548
+ if (hit?.url) {
54549
+ log(`telegram gateway: linear_create_issue: dedup hit key=${dedupKey} agent=${agent}
54550
+ `);
54551
+ return { content: [{ type: "text", text: `Already filed: ${hit.url}` }] };
54552
+ }
54553
+ }
54554
+ }
54555
+ let teamId = teamIdArg;
54556
+ if (!teamId) {
54557
+ const teams = await gql("query { teams(first: 50) { nodes { id key name } } }", {});
54558
+ if (!teams.ok) {
54559
+ return { content: [{ type: "text", text: `Couldn't file to Linear: ${teams.text}` }] };
54560
+ }
54561
+ const nodes = teams.data?.teams?.nodes ?? [];
54562
+ if (nodes.length === 0) {
54563
+ return { content: [{ type: "text", text: "Couldn't file to Linear: the workspace has no teams." }] };
54564
+ }
54565
+ if (nodes.length > 1) {
54566
+ const list2 = nodes.map((t) => `${t.key} (${t.name})`).join(", ");
54567
+ return {
54568
+ content: [
54569
+ { type: "text", text: `Couldn't file to Linear: multiple teams (${list2}) \u2014 set a default team (linear_agent.default_team_id) or pass team_id.` }
54570
+ ]
54571
+ };
54572
+ }
54573
+ teamId = nodes[0].id;
54574
+ }
54575
+ const description = dedupKey ? `${body}${captureDedupMarker(dedupKey)}` : body;
54576
+ const input = { teamId, title, description };
54577
+ if (priority !== undefined)
54578
+ input.priority = priority;
54579
+ const create = await gql("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url } } }", { input });
54580
+ if (!create.ok) {
54581
+ return { content: [{ type: "text", text: `Couldn't file to Linear: ${create.text}` }] };
54582
+ }
54583
+ const issue = create.data?.issueCreate?.issue;
54584
+ if (create.data?.issueCreate?.success === false || !issue?.url) {
54585
+ return { content: [{ type: "text", text: "Couldn't file to Linear: issue not created (success=false)." }] };
54586
+ }
54587
+ log(`telegram gateway: linear_create_issue: filed ${issue.identifier} agent=${agent}${dedupKey ? ` dedup=${dedupKey}` : ""}
54588
+ `);
54589
+ return { content: [{ type: "text", text: `Filed: ${title} \u2192 ${issue.url}` }] };
54590
+ }
54591
+
54090
54592
  // vault-approval-posture.ts
54091
54593
  function resolveVaultApprovalPosture(broker) {
54092
54594
  if (broker?.approvalAuth === "telegram-id") {
@@ -55861,6 +56363,8 @@ function sweepStaleAlwaysAllowCorrelations(now = Date.now()) {
55861
56363
  }
55862
56364
  }
55863
56365
  }
56366
+ var scopedGrants = new Map;
56367
+ var selfAgentName = () => process.env.SWITCHROOM_AGENT_NAME ?? "";
55864
56368
  var pendingAskUser = new Map;
55865
56369
  var pendingReauthFlows = new Map;
55866
56370
  var REAUTH_INTERCEPT_TTL_MS = 600000;
@@ -56095,6 +56599,7 @@ var pendingStateReaper = setInterval(() => {
56095
56599
  if (now > v.expiresAt)
56096
56600
  vaultPassphraseCache.delete(k);
56097
56601
  }
56602
+ sweepScopedGrants(scopedGrants, now);
56098
56603
  for (const [k, v] of deferredSecrets) {
56099
56604
  if (now - v.staged_at > DEFERRED_SECRET_TTL_MS)
56100
56605
  deferredSecrets.delete(k);
@@ -56634,8 +57139,10 @@ if (inboundSpool != null) {
56634
57139
  }
56635
57140
  }
56636
57141
  var pendingPermissionBuffer = createPendingPermissionBuffer();
56637
- function buildPermissionActionRow(requestId, showAlways) {
57142
+ function buildPermissionActionRow(requestId, showAlways, showTimeBox = false) {
56638
57143
  const kb = new import_grammy9.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow once", `perm:allow:${requestId}`);
57144
+ if (showTimeBox)
57145
+ kb.text("\u23F1 30 min", `perm:tmb:${requestId}`);
56639
57146
  if (showAlways)
56640
57147
  kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
56641
57148
  return kb;
@@ -56874,6 +57381,16 @@ var ipcServer = createIpcServer({
56874
57381
  },
56875
57382
  onPermissionRequest(_client, msg) {
56876
57383
  const { requestId, toolName, description, inputPreview } = msg;
57384
+ const scopedTtl = scopedApprovalTtlMs();
57385
+ if (scopedTtl > 0) {
57386
+ const hit = lookupScopedGrant(scopedGrants, selfAgentName(), toolName, inputPreview, Date.now());
57387
+ if (hit) {
57388
+ dispatchPermissionVerdict({ type: "permission", requestId, behavior: "allow" });
57389
+ process.stderr.write(`telegram gateway: scoped-approval auto-allow tool=${toolName} rule="${hit}" request=${requestId} (time-boxed window)
57390
+ `);
57391
+ return;
57392
+ }
57393
+ }
56877
57394
  pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now() });
56878
57395
  const text = formatPermissionCardBody({
56879
57396
  toolName,
@@ -56881,8 +57398,10 @@ var ipcServer = createIpcServer({
56881
57398
  description,
56882
57399
  agentName: _client.agentName
56883
57400
  });
56884
- const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
56885
- const keyboard = buildPermissionActionRow(requestId, showAlways);
57401
+ const scopeChoices = resolveScopedAllowChoices(toolName, inputPreview);
57402
+ const showAlways = scopeChoices != null;
57403
+ const showTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(toolName, inputPreview, scopeChoices) != null;
57404
+ const keyboard = buildPermissionActionRow(requestId, showAlways, showTimeBox);
56886
57405
  const activeTurn = currentTurn;
56887
57406
  const targets = resolvePermissionCardTargets();
56888
57407
  for (const { chatId, threadId } of targets) {
@@ -57294,7 +57813,9 @@ var ALLOWED_TOOLS = new Set([
57294
57813
  "send_gif",
57295
57814
  "vault_request_save",
57296
57815
  "vault_request_access",
57297
- "request_secret"
57816
+ "request_secret",
57817
+ "linear_agent_activity",
57818
+ "linear_create_issue"
57298
57819
  ]);
57299
57820
  async function executeToolCall(tool, args) {
57300
57821
  if (!ALLOWED_TOOLS.has(tool)) {
@@ -57339,6 +57860,10 @@ async function executeToolCall(tool, args) {
57339
57860
  return executeVaultRequestAccess(args);
57340
57861
  case "request_secret":
57341
57862
  return executeRequestSecret(args);
57863
+ case "linear_agent_activity":
57864
+ return executeLinearAgentActivity(args);
57865
+ case "linear_create_issue":
57866
+ return executeLinearCreateIssue(args);
57342
57867
  default:
57343
57868
  throw new Error(`unknown tool: ${tool}`);
57344
57869
  }
@@ -57369,6 +57894,12 @@ async function executeSendChecklist(args) {
57369
57894
  `);
57370
57895
  return { content: [{ type: "text", text: `checklist sent (id: ${sent.message_id})` }] };
57371
57896
  }
57897
+ async function executeLinearAgentActivity(args) {
57898
+ return emitLinearAgentActivity(args);
57899
+ }
57900
+ async function executeLinearCreateIssue(args) {
57901
+ return createLinearIssue(args);
57902
+ }
57372
57903
  async function executeUpdateChecklist(args) {
57373
57904
  const chat_id = args.chat_id;
57374
57905
  if (!chat_id)
@@ -60744,7 +61275,7 @@ function getMyAgentName() {
60744
61275
  const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
60745
61276
  if (fromEnv && fromEnv.trim().length > 0)
60746
61277
  return fromEnv.trim();
60747
- return basename7(process.cwd());
61278
+ return basename9(process.cwd());
60748
61279
  }
60749
61280
  function isSelfTargetingCommand(name) {
60750
61281
  if (name === "all")
@@ -64528,7 +65059,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
64528
65059
  }
64529
65060
  return;
64530
65061
  }
64531
- const m = /^perm:(allow|deny|always|asn|asb|back):([a-km-z]{5})$/.exec(data);
65062
+ const m = /^perm:(allow|deny|always|asn|asb|back|tmb):([a-km-z]{5})$/.exec(data);
64532
65063
  if (!m) {
64533
65064
  await ctx.answerCallbackQuery().catch(() => {});
64534
65065
  return;
@@ -64548,7 +65079,9 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
64548
65079
  }
64549
65080
  let keyboard;
64550
65081
  if (behavior === "back") {
64551
- keyboard = buildPermissionActionRow(request_id, true);
65082
+ const backChoices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
65083
+ const backTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(details.tool_name, details.input_preview, backChoices) != null;
65084
+ keyboard = buildPermissionActionRow(request_id, true, backTimeBox);
64552
65085
  } else {
64553
65086
  const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
64554
65087
  if (choices == null) {
@@ -64689,6 +65222,51 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
64689
65222
  ackText: ackText.slice(0, 200),
64690
65223
  newText: baseText2 ? `${baseText2}
64691
65224
 
65225
+ ${editLabel}` : editLabel,
65226
+ parseMode: "HTML"
65227
+ });
65228
+ return;
65229
+ }
65230
+ if (behavior === "tmb") {
65231
+ const details = pendingPermissions.get(request_id);
65232
+ if (!details) {
65233
+ await ctx.answerCallbackQuery({ text: "Details no longer available." }).catch(() => {});
65234
+ return;
65235
+ }
65236
+ const ttl = scopedApprovalTtlMs();
65237
+ if (ttl <= 0) {
65238
+ await ctx.answerCallbackQuery({ text: "Time-boxed approvals are disabled." }).catch(() => {});
65239
+ return;
65240
+ }
65241
+ const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
65242
+ const tb = resolveTimeBox(details.tool_name, details.input_preview, choices);
65243
+ if (!tb) {
65244
+ await ctx.answerCallbackQuery({ text: "This action can't be time-boxed." }).catch(() => {});
65245
+ return;
65246
+ }
65247
+ const agentName3 = selfAgentName();
65248
+ if (!agentName3) {
65249
+ await ctx.answerCallbackQuery({ text: "Time-box needs SWITCHROOM_AGENT_NAME \u2014 gateway is misconfigured." }).catch(() => {});
65250
+ return;
65251
+ }
65252
+ pendingPermissions.delete(request_id);
65253
+ dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior: "allow" });
65254
+ recordScopedGrant(scopedGrants, agentName3, tb.rule, Date.now(), ttl);
65255
+ resumeReactionAfterVerdict();
65256
+ postPermissionResumeMessage({
65257
+ behavior: "allow",
65258
+ action: naturalAction(details.tool_name, details.input_preview)
65259
+ });
65260
+ process.stderr.write(`telegram gateway: scoped-approval granted rule="${tb.rule}" agent=${agentName3} ttl_ms=${ttl} (request_id=${request_id})
65261
+ `);
65262
+ const mins = Math.max(1, Math.round(ttl / 60000));
65263
+ const sourceMsg = ctx.callbackQuery?.message;
65264
+ const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
65265
+ const editLabel = `\u23F1 <b>Allowed for ${mins} min \u2014 ${escapeHtmlForTg(tb.breadth)}</b> \xB7 re-asks after that, and now for anything else`;
65266
+ await finalizeCallback(ctx, {
65267
+ ackText: `\u23F1 Allowed for ${mins} min`.slice(0, 200),
65268
+ newText: baseText2 ? `${baseText2}
65269
+
64692
65270
  ${editLabel}` : editLabel,
64693
65271
  parseMode: "HTML"
64694
65272
  });