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.
- package/dist/agent-scheduler/index.js +12 -1
- package/dist/auth-broker/index.js +12 -1
- package/dist/cli/notion-write-pretool.mjs +12 -1
- package/dist/cli/switchroom.js +69 -8
- package/dist/host-control/main.js +12 -1
- package/dist/vault/approvals/kernel-server.js +12 -1
- package/dist/vault/broker/server.js +12 -1
- package/package.json +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +37 -0
- package/telegram-plugin/bridge/bridge.ts +31 -0
- package/telegram-plugin/dist/bridge/bridge.js +30 -0
- package/telegram-plugin/dist/gateway/gateway.js +434 -50
- package/telegram-plugin/dist/server.js +30 -0
- package/telegram-plugin/gateway/gateway.ts +123 -6
- package/telegram-plugin/gateway/linear-activity.ts +145 -0
- package/telegram-plugin/scoped-approval.ts +253 -0
- package/telegram-plugin/tests/linear-agent-activity.test.ts +1 -1
- package/telegram-plugin/tests/linear-create-issue.test.ts +211 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +13 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +254 -0
|
@@ -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
|
|
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
|
|
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 =
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
53098
|
+
return `edit ${basename6(arg)}`;
|
|
53000
53099
|
if (t === "Write")
|
|
53001
|
-
return `write ${
|
|
53100
|
+
return `write ${basename6(arg)}`;
|
|
53002
53101
|
if (t === "Read")
|
|
53003
|
-
return `read ${
|
|
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
|
|
53036
|
-
return
|
|
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 =
|
|
53042
|
-
return p ?
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
53052
53151
|
}
|
|
53053
|
-
function
|
|
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
|
|
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
|
|
53072
|
-
const 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
|
|
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 =
|
|
53208
|
+
const input = parseInput3(inputPreview);
|
|
53110
53209
|
if (toolName === "Skill") {
|
|
53111
|
-
const skill = input ?
|
|
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 =
|
|
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 ?
|
|
53135
|
-
const tok = cmd ?
|
|
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
|
|
53164
|
-
return
|
|
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
|
|
53265
|
+
function filePathFrom2(input) {
|
|
53167
53266
|
if (!input)
|
|
53168
53267
|
return null;
|
|
53169
|
-
return
|
|
53268
|
+
return readString3(input, "file_path") ?? readString3(input, "notebook_path");
|
|
53170
53269
|
}
|
|
53171
|
-
function
|
|
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
|
|
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
|
|
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
|
|
53199
|
-
const 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
|
|
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.
|
|
53881
|
-
var COMMIT_SHA = "
|
|
53882
|
-
var COMMIT_DATE = "2026-06-
|
|
53883
|
-
var LATEST_PR =
|
|
53884
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
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
|
|
57073
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
});
|