switchroom 0.15.12 → 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.
@@ -23996,7 +23996,8 @@ var init_schema = __esm(() => {
23996
23996
  linear_agent: exports_external.object({
23997
23997
  enabled: exports_external.boolean(),
23998
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.")
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>`.")
24000
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."),
24001
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."),
24002
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`."),
@@ -24880,6 +24881,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
24880
24881
  }
24881
24882
  merged.reaction_dispatch = combined;
24882
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
+ }
24883
24894
  if (defaults.resources || merged.resources) {
24884
24895
  const d = defaults.resources ?? {};
24885
24896
  const a = merged.resources ?? {};
@@ -31455,7 +31466,7 @@ import {
31455
31466
  appendFileSync as appendFileSync5
31456
31467
  } from "fs";
31457
31468
  import { homedir as homedir14 } from "os";
31458
- 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";
31459
31470
 
31460
31471
  // plugin-logger.ts
31461
31472
  import { appendFileSync, mkdirSync, renameSync, statSync, existsSync } from "fs";
@@ -45826,6 +45837,16 @@ function mergeAgentConfig2(defaultsIn, agentIn) {
45826
45837
  }
45827
45838
  merged.reaction_dispatch = combined;
45828
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
+ }
45829
45850
  if (defaults.resources || merged.resources) {
45830
45851
  const d = defaults.resources ?? {};
45831
45852
  const a = merged.resources ?? {};
@@ -52781,9 +52802,10 @@ function defaultReadEvents(stateDir) {
52781
52802
  return readAll(stateDir);
52782
52803
  }
52783
52804
  // permission-title.ts
52784
- import { basename as basename5 } from "node:path";
52805
+ import { basename as basename6 } from "node:path";
52785
52806
 
52786
52807
  // permission-rule.ts
52808
+ import { basename as basename5 } from "node:path";
52787
52809
  var FILE_TOOLS = new Set([
52788
52810
  "Edit",
52789
52811
  "Write",
@@ -52804,6 +52826,83 @@ var BROAD_ONLY_TOOLS = new Set([
52804
52826
  function prettyMcpServer(server) {
52805
52827
  return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
52806
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
+ }
52807
52906
 
52808
52907
  // permission-title.ts
52809
52908
  init_redact();
@@ -52862,7 +52961,7 @@ function formatPermissionCardBody(opts) {
52862
52961
  `);
52863
52962
  }
52864
52963
  function naturalAction(toolName, inputPreview) {
52865
- const input = parseInput(inputPreview);
52964
+ const input = parseInput2(inputPreview);
52866
52965
  if (toolName.startsWith("mcp__"))
52867
52966
  return naturalMcpAction(toolName, input);
52868
52967
  switch (toolName) {
@@ -52881,24 +52980,24 @@ function naturalAction(toolName, inputPreview) {
52881
52980
  return f ? `read: ${f}` : "read files";
52882
52981
  }
52883
52982
  case "Bash": {
52884
- const c = input ? readString(input, "command") : null;
52983
+ const c = input ? readString2(input, "command") : null;
52885
52984
  return c ? `run: ${truncate6(c, COMMAND_TITLE_MAX)}` : "run shell commands";
52886
52985
  }
52887
52986
  case "Skill": {
52888
- const s = input ? resolveSkillName(input) : null;
52987
+ const s = input ? resolveSkillName2(input) : null;
52889
52988
  return s ? `use the ${s} skill` : "use a skill";
52890
52989
  }
52891
52990
  case "Glob":
52892
52991
  case "Grep": {
52893
- const p = input ? readString(input, "pattern") : null;
52992
+ const p = input ? readString2(input, "pattern") : null;
52894
52993
  return p ? `search files for: ${truncate6(p, COMMAND_TITLE_MAX)}` : "search files";
52895
52994
  }
52896
52995
  case "WebSearch": {
52897
- const q = input ? readString(input, "query") : null;
52996
+ const q = input ? readString2(input, "query") : null;
52898
52997
  return q ? `search the web for: ${truncate6(q, COMMAND_TITLE_MAX)}` : "search the web";
52899
52998
  }
52900
52999
  case "WebFetch": {
52901
- const u = input ? readString(input, "url") : null;
53000
+ const u = input ? readString2(input, "url") : null;
52902
53001
  return u ? `fetch a web page: ${truncate6(u, COMMAND_TITLE_MAX)}` : "fetch a web page";
52903
53002
  }
52904
53003
  case "Task":
@@ -52936,7 +53035,7 @@ function restResourcePhrase(server, verb, input) {
52936
53035
  return null;
52937
53036
  let path = null;
52938
53037
  for (const key of RESOURCE_KEYS) {
52939
- path = readString(input, key);
53038
+ path = readString2(input, key);
52940
53039
  if (path)
52941
53040
  break;
52942
53041
  }
@@ -52952,7 +53051,7 @@ function mcpArgSummary(toolName, inputPreview) {
52952
53051
  const server = toolName.split("__")[1] ?? "";
52953
53052
  if (INTERNAL_MCP_SERVERS.has(server))
52954
53053
  return null;
52955
- const input = parseInput(inputPreview);
53054
+ const input = parseInput2(inputPreview);
52956
53055
  if (!input)
52957
53056
  return null;
52958
53057
  const payload = input.body ?? input.query;
@@ -52996,11 +53095,11 @@ function describeGrant(toolName, inputPreview, option) {
52996
53095
  return m ? `run ${m[1]} commands` : "run that command";
52997
53096
  }
52998
53097
  if (t === "Edit" || t === "MultiEdit" || t === "NotebookEdit")
52999
- return `edit ${basename5(arg)}`;
53098
+ return `edit ${basename6(arg)}`;
53000
53099
  if (t === "Write")
53001
- return `write ${basename5(arg)}`;
53100
+ return `write ${basename6(arg)}`;
53002
53101
  if (t === "Read")
53003
- return `read ${basename5(arg)}`;
53102
+ return `read ${basename6(arg)}`;
53004
53103
  return naturalAction(toolName, inputPreview);
53005
53104
  }
53006
53105
  switch (rule) {
@@ -53032,14 +53131,14 @@ function formatPermissionResumeMessage(opts) {
53032
53131
  }
53033
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.`;
53034
53133
  }
53035
- function resolveSkillName(input) {
53036
- 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);
53037
53136
  }
53038
53137
  function fileBase(input) {
53039
53138
  if (!input)
53040
53139
  return null;
53041
- const p = readString(input, "file_path") ?? readString(input, "notebook_path");
53042
- return p ? basename5(p) : null;
53140
+ const p = readString2(input, "file_path") ?? readString2(input, "notebook_path");
53141
+ return p ? basename6(p) : null;
53043
53142
  }
53044
53143
  function lowerFirst(text) {
53045
53144
  return text.length > 0 ? text.charAt(0).toLowerCase() + text.slice(1) : text;
@@ -53050,7 +53149,7 @@ function capFirst(text) {
53050
53149
  function escapeTgHtml(text) {
53051
53150
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
53052
53151
  }
53053
- function parseInput(raw) {
53152
+ function parseInput2(raw) {
53054
53153
  if (!raw || typeof raw !== "string")
53055
53154
  return null;
53056
53155
  const trimmed = raw.trim();
@@ -53064,12 +53163,12 @@ function parseInput(raw) {
53064
53163
  } catch {}
53065
53164
  return null;
53066
53165
  }
53067
- function readString(input, key) {
53166
+ function readString2(input, key) {
53068
53167
  const value = input[key];
53069
53168
  return typeof value === "string" && value.length > 0 ? value : null;
53070
53169
  }
53071
- function skillBasenameFromPath(input) {
53072
- const path = readString(input, "path") ?? readString(input, "skill_path");
53170
+ function skillBasenameFromPath2(input) {
53171
+ const path = readString2(input, "path") ?? readString2(input, "skill_path");
53073
53172
  if (!path)
53074
53173
  return null;
53075
53174
  const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
@@ -53085,7 +53184,7 @@ function truncate6(text, max) {
53085
53184
  }
53086
53185
 
53087
53186
  // permission-rule.ts
53088
- import { basename as basename6 } from "node:path";
53187
+ import { basename as basename7 } from "node:path";
53089
53188
  var FILE_TOOLS2 = new Set([
53090
53189
  "Edit",
53091
53190
  "Write",
@@ -53106,9 +53205,9 @@ var BROAD_ONLY_TOOLS2 = new Set([
53106
53205
  function resolveScopedAllowChoices(toolName, inputPreview) {
53107
53206
  if (!toolName)
53108
53207
  return null;
53109
- const input = parseInput2(inputPreview);
53208
+ const input = parseInput3(inputPreview);
53110
53209
  if (toolName === "Skill") {
53111
- const skill = input ? resolveSkillName2(input) : null;
53210
+ const skill = input ? resolveSkillName3(input) : null;
53112
53211
  if (!skill)
53113
53212
  return null;
53114
53213
  if (!/^[A-Za-z0-9._\-+]+$/.test(skill))
@@ -53119,7 +53218,7 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
53119
53218
  };
53120
53219
  }
53121
53220
  if (FILE_TOOLS2.has(toolName)) {
53122
- const path = filePathFrom(input);
53221
+ const path = filePathFrom2(input);
53123
53222
  const broad = { rule: toolName, buttonLabel: "Any file", broad: true };
53124
53223
  if (path) {
53125
53224
  return {
@@ -53131,8 +53230,8 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
53131
53230
  }
53132
53231
  if (toolName === "Bash") {
53133
53232
  const broad = { rule: "Bash", buttonLabel: "Any command", broad: true };
53134
- const cmd = input ? readString2(input, "command") : null;
53135
- const tok = cmd ? bashFirstToken(cmd) : null;
53233
+ const cmd = input ? readString3(input, "command") : null;
53234
+ const tok = cmd ? bashFirstToken2(cmd) : null;
53136
53235
  if (tok) {
53137
53236
  return {
53138
53237
  specific: { rule: `Bash(${tok}:*)`, buttonLabel: `${tok} commands`, broad: false },
@@ -53160,15 +53259,15 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
53160
53259
  function prettyMcpServer2(server) {
53161
53260
  return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
53162
53261
  }
53163
- function resolveSkillName2(input) {
53164
- 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);
53165
53264
  }
53166
- function filePathFrom(input) {
53265
+ function filePathFrom2(input) {
53167
53266
  if (!input)
53168
53267
  return null;
53169
- return readString2(input, "file_path") ?? readString2(input, "notebook_path");
53268
+ return readString3(input, "file_path") ?? readString3(input, "notebook_path");
53170
53269
  }
53171
- function bashFirstToken(command) {
53270
+ function bashFirstToken2(command) {
53172
53271
  const m = /^\s*([^\s|&;<>()`$]+)/.exec(command);
53173
53272
  if (!m)
53174
53273
  return null;
@@ -53177,7 +53276,7 @@ function bashFirstToken(command) {
53177
53276
  return null;
53178
53277
  return /^[A-Za-z0-9._\-\/]+$/.test(tok) ? tok : null;
53179
53278
  }
53180
- function parseInput2(raw) {
53279
+ function parseInput3(raw) {
53181
53280
  if (!raw || typeof raw !== "string")
53182
53281
  return null;
53183
53282
  const trimmed = raw.trim();
@@ -53191,21 +53290,136 @@ function parseInput2(raw) {
53191
53290
  } catch {}
53192
53291
  return null;
53193
53292
  }
53194
- function readString2(input, key) {
53293
+ function readString3(input, key) {
53195
53294
  const value = input[key];
53196
53295
  return typeof value === "string" && value.length > 0 ? value : null;
53197
53296
  }
53198
- function skillBasenameFromPath2(input) {
53199
- const path = readString2(input, "path") ?? readString2(input, "skill_path");
53297
+ function skillBasenameFromPath3(input) {
53298
+ const path = readString3(input, "path") ?? readString3(input, "skill_path");
53200
53299
  if (!path)
53201
53300
  return null;
53202
53301
  const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
53203
- return basename6(trimmed) || null;
53302
+ return basename7(trimmed) || null;
53204
53303
  }
53205
53304
  function isRulePersisted(resolvedAllow, ruleRule) {
53206
53305
  return resolvedAllow.includes(ruleRule);
53207
53306
  }
53208
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
+
53209
53423
  // permission-diff.ts
53210
53424
  var TARGET_HEADER_A = "--- a/switchroom.yaml";
53211
53425
  var TARGET_HEADER_B = "+++ b/switchroom.yaml";
@@ -53877,11 +54091,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
53877
54091
  }
53878
54092
 
53879
54093
  // ../src/build-info.ts
53880
- var VERSION = "0.15.12";
53881
- var COMMIT_SHA = "18b7b6e6";
53882
- var COMMIT_DATE = "2026-06-13T04:55:14Z";
53883
- var LATEST_PR = 2311;
53884
- 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;
53885
54099
 
53886
54100
  // gateway/boot-version.ts
53887
54101
  function formatRelativeAgo(iso) {
@@ -54274,6 +54488,106 @@ async function emitLinearAgentActivity(args, deps = {}) {
54274
54488
  `);
54275
54489
  return { content: [{ type: "text", text: `Linear ${type} emitted on session ${sessionId}` }] };
54276
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
+ }
54277
54591
 
54278
54592
  // vault-approval-posture.ts
54279
54593
  function resolveVaultApprovalPosture(broker) {
@@ -56049,6 +56363,8 @@ function sweepStaleAlwaysAllowCorrelations(now = Date.now()) {
56049
56363
  }
56050
56364
  }
56051
56365
  }
56366
+ var scopedGrants = new Map;
56367
+ var selfAgentName = () => process.env.SWITCHROOM_AGENT_NAME ?? "";
56052
56368
  var pendingAskUser = new Map;
56053
56369
  var pendingReauthFlows = new Map;
56054
56370
  var REAUTH_INTERCEPT_TTL_MS = 600000;
@@ -56283,6 +56599,7 @@ var pendingStateReaper = setInterval(() => {
56283
56599
  if (now > v.expiresAt)
56284
56600
  vaultPassphraseCache.delete(k);
56285
56601
  }
56602
+ sweepScopedGrants(scopedGrants, now);
56286
56603
  for (const [k, v] of deferredSecrets) {
56287
56604
  if (now - v.staged_at > DEFERRED_SECRET_TTL_MS)
56288
56605
  deferredSecrets.delete(k);
@@ -56822,8 +57139,10 @@ if (inboundSpool != null) {
56822
57139
  }
56823
57140
  }
56824
57141
  var pendingPermissionBuffer = createPendingPermissionBuffer();
56825
- function buildPermissionActionRow(requestId, showAlways) {
57142
+ function buildPermissionActionRow(requestId, showAlways, showTimeBox = false) {
56826
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}`);
56827
57146
  if (showAlways)
56828
57147
  kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
56829
57148
  return kb;
@@ -57062,6 +57381,16 @@ var ipcServer = createIpcServer({
57062
57381
  },
57063
57382
  onPermissionRequest(_client, msg) {
57064
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
+ }
57065
57394
  pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now() });
57066
57395
  const text = formatPermissionCardBody({
57067
57396
  toolName,
@@ -57069,8 +57398,10 @@ var ipcServer = createIpcServer({
57069
57398
  description,
57070
57399
  agentName: _client.agentName
57071
57400
  });
57072
- const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
57073
- 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);
57074
57405
  const activeTurn = currentTurn;
57075
57406
  const targets = resolvePermissionCardTargets();
57076
57407
  for (const { chatId, threadId } of targets) {
@@ -57483,7 +57814,8 @@ var ALLOWED_TOOLS = new Set([
57483
57814
  "vault_request_save",
57484
57815
  "vault_request_access",
57485
57816
  "request_secret",
57486
- "linear_agent_activity"
57817
+ "linear_agent_activity",
57818
+ "linear_create_issue"
57487
57819
  ]);
57488
57820
  async function executeToolCall(tool, args) {
57489
57821
  if (!ALLOWED_TOOLS.has(tool)) {
@@ -57530,6 +57862,8 @@ async function executeToolCall(tool, args) {
57530
57862
  return executeRequestSecret(args);
57531
57863
  case "linear_agent_activity":
57532
57864
  return executeLinearAgentActivity(args);
57865
+ case "linear_create_issue":
57866
+ return executeLinearCreateIssue(args);
57533
57867
  default:
57534
57868
  throw new Error(`unknown tool: ${tool}`);
57535
57869
  }
@@ -57563,6 +57897,9 @@ async function executeSendChecklist(args) {
57563
57897
  async function executeLinearAgentActivity(args) {
57564
57898
  return emitLinearAgentActivity(args);
57565
57899
  }
57900
+ async function executeLinearCreateIssue(args) {
57901
+ return createLinearIssue(args);
57902
+ }
57566
57903
  async function executeUpdateChecklist(args) {
57567
57904
  const chat_id = args.chat_id;
57568
57905
  if (!chat_id)
@@ -60938,7 +61275,7 @@ function getMyAgentName() {
60938
61275
  const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
60939
61276
  if (fromEnv && fromEnv.trim().length > 0)
60940
61277
  return fromEnv.trim();
60941
- return basename7(process.cwd());
61278
+ return basename9(process.cwd());
60942
61279
  }
60943
61280
  function isSelfTargetingCommand(name) {
60944
61281
  if (name === "all")
@@ -64722,7 +65059,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
64722
65059
  }
64723
65060
  return;
64724
65061
  }
64725
- 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);
64726
65063
  if (!m) {
64727
65064
  await ctx.answerCallbackQuery().catch(() => {});
64728
65065
  return;
@@ -64742,7 +65079,9 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
64742
65079
  }
64743
65080
  let keyboard;
64744
65081
  if (behavior === "back") {
64745
- 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);
64746
65085
  } else {
64747
65086
  const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
64748
65087
  if (choices == null) {
@@ -64883,6 +65222,51 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
64883
65222
  ackText: ackText.slice(0, 200),
64884
65223
  newText: baseText2 ? `${baseText2}
64885
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
+
64886
65270
  ${editLabel}` : editLabel,
64887
65271
  parseMode: "HTML"
64888
65272
  });