switchroom 0.14.12 → 0.14.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/cli/switchroom.js +13 -11
- package/dist/host-control/main.js +80 -6
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +61 -8
- package/telegram-plugin/dist/gateway/gateway.js +283 -161
- package/telegram-plugin/dist/server.js +64 -9
- package/telegram-plugin/gateway/gateway.ts +78 -66
- package/telegram-plugin/gateway/ipc-protocol.ts +4 -2
- package/telegram-plugin/permission-rule.ts +200 -122
- package/telegram-plugin/permission-title.ts +209 -197
- package/telegram-plugin/tests/always-allow-grant.test.ts +86 -54
- package/telegram-plugin/tests/always-allow-persist.test.ts +35 -34
- package/telegram-plugin/tests/permission-rule.test.ts +185 -127
- package/telegram-plugin/tests/permission-title.test.ts +109 -195
|
@@ -50004,10 +50004,32 @@ function defaultReadEvents(stateDir) {
|
|
|
50004
50004
|
}
|
|
50005
50005
|
// permission-title.ts
|
|
50006
50006
|
import { basename as basename5 } from "node:path";
|
|
50007
|
-
|
|
50008
|
-
|
|
50007
|
+
|
|
50008
|
+
// permission-rule.ts
|
|
50009
|
+
var FILE_TOOLS = new Set([
|
|
50010
|
+
"Edit",
|
|
50011
|
+
"Write",
|
|
50012
|
+
"MultiEdit",
|
|
50013
|
+
"NotebookEdit",
|
|
50014
|
+
"Read"
|
|
50015
|
+
]);
|
|
50016
|
+
var BROAD_ONLY_TOOLS = new Set([
|
|
50017
|
+
"Glob",
|
|
50018
|
+
"Grep",
|
|
50019
|
+
"WebFetch",
|
|
50020
|
+
"WebSearch",
|
|
50021
|
+
"Task",
|
|
50022
|
+
"Agent",
|
|
50023
|
+
"TodoWrite",
|
|
50024
|
+
"ExitPlanMode"
|
|
50025
|
+
]);
|
|
50026
|
+
function prettyMcpServer(server) {
|
|
50027
|
+
return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
50028
|
+
}
|
|
50029
|
+
|
|
50030
|
+
// permission-title.ts
|
|
50031
|
+
var COMMAND_TITLE_MAX = 48;
|
|
50009
50032
|
var DESCRIPTION_LINE_MAX = 240;
|
|
50010
|
-
var INPUT_VALUE_MAX = 60;
|
|
50011
50033
|
var MCP_TOOL_DESCRIPTIONS = {
|
|
50012
50034
|
"mcp__agent-config__config_get": "Read its own merged config",
|
|
50013
50035
|
"mcp__agent-config__cron_list": "List its own scheduled tasks",
|
|
@@ -50027,103 +50049,153 @@ var MCP_TOOL_DESCRIPTIONS = {
|
|
|
50027
50049
|
mcp__hostd__update_apply: "Apply a fleet-wide update (pull + recreate)",
|
|
50028
50050
|
mcp__hindsight__recall: "Recall relevant memories",
|
|
50029
50051
|
mcp__hindsight__retain: "Retain a memory",
|
|
50030
|
-
mcp__hindsight__reflect: "Reflect across its memory bank"
|
|
50052
|
+
mcp__hindsight__reflect: "Reflect across its memory bank",
|
|
50053
|
+
mcp__perplexity__search: "Search the web",
|
|
50054
|
+
mcp__perplexity__ask: "Ask the web"
|
|
50031
50055
|
};
|
|
50032
|
-
|
|
50033
|
-
|
|
50034
|
-
|
|
50035
|
-
|
|
50036
|
-
|
|
50037
|
-
|
|
50038
|
-
|
|
50039
|
-
|
|
50040
|
-
|
|
50041
|
-
|
|
50042
|
-
|
|
50043
|
-
|
|
50044
|
-
|
|
50045
|
-
return argHint ? `${base} (${argHint})` : base;
|
|
50056
|
+
var INTERNAL_MCP_SERVERS = new Set([
|
|
50057
|
+
"agent-config",
|
|
50058
|
+
"hostd",
|
|
50059
|
+
"hindsight",
|
|
50060
|
+
"switchroom-telegram"
|
|
50061
|
+
]);
|
|
50062
|
+
function formatPermissionCardBody(opts) {
|
|
50063
|
+
const action = naturalAction(opts.toolName, opts.inputPreview);
|
|
50064
|
+
const lines = [];
|
|
50065
|
+
if (opts.agentName && opts.agentName.length > 0) {
|
|
50066
|
+
lines.push(`\uD83D\uDD10 <b>${escapeTgHtml(capFirst(opts.agentName))}</b> wants to ${escapeTgHtml(action)}`);
|
|
50067
|
+
} else {
|
|
50068
|
+
lines.push(`\uD83D\uDD10 ${escapeTgHtml(capFirst(action))}`);
|
|
50046
50069
|
}
|
|
50070
|
+
const rawWhy = (opts.description ?? "").replace(/\s+/g, " ").trim();
|
|
50071
|
+
const truncatedWhy = rawWhy.length > DESCRIPTION_LINE_MAX ? rawWhy.slice(0, DESCRIPTION_LINE_MAX - 1) + "\u2026" : rawWhy;
|
|
50072
|
+
lines.push(truncatedWhy.length > 0 ? `why: <i>${escapeTgHtml(truncatedWhy)}</i>` : `why: <i>not provided</i>`);
|
|
50073
|
+
return lines.join(`
|
|
50074
|
+
`);
|
|
50075
|
+
}
|
|
50076
|
+
function naturalAction(toolName, inputPreview) {
|
|
50047
50077
|
const input = parseInput(inputPreview);
|
|
50048
|
-
if (
|
|
50049
|
-
return toolName;
|
|
50078
|
+
if (toolName.startsWith("mcp__"))
|
|
50079
|
+
return naturalMcpAction(toolName, input);
|
|
50050
50080
|
switch (toolName) {
|
|
50051
|
-
case "Skill": {
|
|
50052
|
-
const skill = readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
|
|
50053
|
-
if (skill)
|
|
50054
|
-
return `${toolName} (${skill})`;
|
|
50055
|
-
const command = readString(input, "command");
|
|
50056
|
-
if (command)
|
|
50057
|
-
return `${toolName}: ${truncate6(command, COMMAND_TITLE_MAX)}`;
|
|
50058
|
-
const argHint = firstScalarArgHint(input);
|
|
50059
|
-
return argHint ? `${toolName} (${argHint})` : toolName;
|
|
50060
|
-
}
|
|
50061
|
-
case "Bash": {
|
|
50062
|
-
const command = readString(input, "command");
|
|
50063
|
-
return command ? `${toolName}: ${truncate6(command, COMMAND_TITLE_MAX)}` : toolName;
|
|
50064
|
-
}
|
|
50065
|
-
case "Read":
|
|
50066
50081
|
case "Edit":
|
|
50067
|
-
case "Write":
|
|
50068
50082
|
case "MultiEdit":
|
|
50069
50083
|
case "NotebookEdit": {
|
|
50070
|
-
const
|
|
50071
|
-
return
|
|
50084
|
+
const f = fileBase(input);
|
|
50085
|
+
return f ? `edit: ${f}` : "edit files";
|
|
50086
|
+
}
|
|
50087
|
+
case "Write": {
|
|
50088
|
+
const f = fileBase(input);
|
|
50089
|
+
return f ? `write: ${f}` : "write files";
|
|
50090
|
+
}
|
|
50091
|
+
case "Read": {
|
|
50092
|
+
const f = fileBase(input);
|
|
50093
|
+
return f ? `read: ${f}` : "read files";
|
|
50094
|
+
}
|
|
50095
|
+
case "Bash": {
|
|
50096
|
+
const c = input ? readString(input, "command") : null;
|
|
50097
|
+
return c ? `run: ${truncate6(c, COMMAND_TITLE_MAX)}` : "run shell commands";
|
|
50098
|
+
}
|
|
50099
|
+
case "Skill": {
|
|
50100
|
+
const s = input ? resolveSkillName(input) : null;
|
|
50101
|
+
return s ? `use the ${s} skill` : "use a skill";
|
|
50072
50102
|
}
|
|
50073
50103
|
case "Glob":
|
|
50074
50104
|
case "Grep": {
|
|
50075
|
-
const
|
|
50076
|
-
return
|
|
50105
|
+
const p = input ? readString(input, "pattern") : null;
|
|
50106
|
+
return p ? `search files for: ${truncate6(p, COMMAND_TITLE_MAX)}` : "search files";
|
|
50077
50107
|
}
|
|
50078
|
-
case "WebFetch":
|
|
50079
50108
|
case "WebSearch": {
|
|
50080
|
-
const
|
|
50081
|
-
return
|
|
50109
|
+
const q = input ? readString(input, "query") : null;
|
|
50110
|
+
return q ? `search the web for: ${truncate6(q, COMMAND_TITLE_MAX)}` : "search the web";
|
|
50082
50111
|
}
|
|
50112
|
+
case "WebFetch": {
|
|
50113
|
+
const u = input ? readString(input, "url") : null;
|
|
50114
|
+
return u ? `fetch a web page: ${truncate6(u, COMMAND_TITLE_MAX)}` : "fetch a web page";
|
|
50115
|
+
}
|
|
50116
|
+
case "Task":
|
|
50117
|
+
case "Agent":
|
|
50118
|
+
return "dispatch a sub-agent";
|
|
50119
|
+
case "TodoWrite":
|
|
50120
|
+
return "update its task list";
|
|
50121
|
+
case "ExitPlanMode":
|
|
50122
|
+
return "exit plan mode";
|
|
50083
50123
|
default:
|
|
50084
|
-
return toolName
|
|
50085
|
-
}
|
|
50086
|
-
}
|
|
50087
|
-
function
|
|
50088
|
-
const
|
|
50089
|
-
const
|
|
50090
|
-
const
|
|
50091
|
-
|
|
50092
|
-
|
|
50093
|
-
|
|
50094
|
-
|
|
50095
|
-
|
|
50096
|
-
|
|
50097
|
-
|
|
50124
|
+
return `use ${toolName}`;
|
|
50125
|
+
}
|
|
50126
|
+
}
|
|
50127
|
+
function naturalMcpAction(toolName, input) {
|
|
50128
|
+
const parts = toolName.split("__");
|
|
50129
|
+
const server = parts.length >= 2 ? parts[1] : "";
|
|
50130
|
+
const curated = MCP_TOOL_DESCRIPTIONS[toolName];
|
|
50131
|
+
if (curated) {
|
|
50132
|
+
const phrase = lowerFirst(curated);
|
|
50133
|
+
return INTERNAL_MCP_SERVERS.has(server) ? phrase : `${phrase} (${prettyMcpServer(server)})`;
|
|
50134
|
+
}
|
|
50135
|
+
if (parts.length >= 3) {
|
|
50136
|
+
const verb = parts.slice(2).join("__").replace(/_/g, " ");
|
|
50137
|
+
return INTERNAL_MCP_SERVERS.has(server) ? verb : `${verb} (${prettyMcpServer(server)})`;
|
|
50138
|
+
}
|
|
50139
|
+
return `use ${toolName}`;
|
|
50140
|
+
}
|
|
50141
|
+
function describeGrant(toolName, inputPreview, option) {
|
|
50142
|
+
const rule = option.rule;
|
|
50143
|
+
if (rule.endsWith("__*") && rule.startsWith("mcp__")) {
|
|
50144
|
+
const server = rule.split("__")[1] ?? "";
|
|
50145
|
+
return `use any ${prettyMcpServer(server)} tool`;
|
|
50146
|
+
}
|
|
50147
|
+
const scoped = /^([A-Za-z]+)\((.+)\)$/.exec(rule);
|
|
50148
|
+
if (scoped) {
|
|
50149
|
+
const t = scoped[1];
|
|
50150
|
+
const arg = scoped[2];
|
|
50151
|
+
if (t === "Skill")
|
|
50152
|
+
return `use the ${arg} skill`;
|
|
50153
|
+
if (t === "Bash") {
|
|
50154
|
+
const m = /^([^:]+):\*$/.exec(arg);
|
|
50155
|
+
return m ? `run ${m[1]} commands` : "run that command";
|
|
50156
|
+
}
|
|
50157
|
+
if (t === "Edit" || t === "MultiEdit" || t === "NotebookEdit")
|
|
50158
|
+
return `edit ${basename5(arg)}`;
|
|
50159
|
+
if (t === "Write")
|
|
50160
|
+
return `write ${basename5(arg)}`;
|
|
50161
|
+
if (t === "Read")
|
|
50162
|
+
return `read ${basename5(arg)}`;
|
|
50163
|
+
return naturalAction(toolName, inputPreview);
|
|
50164
|
+
}
|
|
50165
|
+
switch (rule) {
|
|
50166
|
+
case "Edit":
|
|
50167
|
+
case "MultiEdit":
|
|
50168
|
+
case "NotebookEdit":
|
|
50169
|
+
return "edit any file";
|
|
50170
|
+
case "Write":
|
|
50171
|
+
return "write any file";
|
|
50172
|
+
case "Read":
|
|
50173
|
+
return "read any file";
|
|
50174
|
+
case "Bash":
|
|
50175
|
+
return "run any command";
|
|
50176
|
+
case "Skill":
|
|
50177
|
+
return "use any skill";
|
|
50178
|
+
default:
|
|
50179
|
+
return naturalAction(toolName, inputPreview);
|
|
50098
50180
|
}
|
|
50099
|
-
return lines.join(`
|
|
50100
|
-
`);
|
|
50101
50181
|
}
|
|
50102
|
-
function
|
|
50103
|
-
return
|
|
50182
|
+
function resolveSkillName(input) {
|
|
50183
|
+
return readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
|
|
50104
50184
|
}
|
|
50105
|
-
function
|
|
50185
|
+
function fileBase(input) {
|
|
50106
50186
|
if (!input)
|
|
50107
50187
|
return null;
|
|
50108
|
-
const
|
|
50109
|
-
|
|
50110
|
-
|
|
50111
|
-
|
|
50112
|
-
|
|
50113
|
-
|
|
50114
|
-
|
|
50115
|
-
|
|
50116
|
-
|
|
50117
|
-
|
|
50118
|
-
|
|
50119
|
-
if (typeof value === "string" && value.length > 0) {
|
|
50120
|
-
return `${key}: ${truncate6(value, INPUT_VALUE_MAX)}`;
|
|
50121
|
-
}
|
|
50122
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
50123
|
-
return `${key}: ${String(value)}`;
|
|
50124
|
-
}
|
|
50125
|
-
}
|
|
50126
|
-
return null;
|
|
50188
|
+
const p = readString(input, "file_path") ?? readString(input, "notebook_path");
|
|
50189
|
+
return p ? basename5(p) : null;
|
|
50190
|
+
}
|
|
50191
|
+
function lowerFirst(text) {
|
|
50192
|
+
return text.length > 0 ? text.charAt(0).toLowerCase() + text.slice(1) : text;
|
|
50193
|
+
}
|
|
50194
|
+
function capFirst(text) {
|
|
50195
|
+
return text.length > 0 ? text.charAt(0).toUpperCase() + text.slice(1) : text;
|
|
50196
|
+
}
|
|
50197
|
+
function escapeTgHtml(text) {
|
|
50198
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
50127
50199
|
}
|
|
50128
50200
|
function parseInput(raw) {
|
|
50129
50201
|
if (!raw || typeof raw !== "string")
|
|
@@ -50149,8 +50221,8 @@ function skillBasenameFromPath(input) {
|
|
|
50149
50221
|
return null;
|
|
50150
50222
|
const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
50151
50223
|
const lastSlash = trimmed.lastIndexOf("/");
|
|
50152
|
-
const
|
|
50153
|
-
return
|
|
50224
|
+
const base = lastSlash >= 0 ? trimmed.slice(lastSlash + 1) : trimmed;
|
|
50225
|
+
return base.length > 0 ? base : null;
|
|
50154
50226
|
}
|
|
50155
50227
|
function truncate6(text, max) {
|
|
50156
50228
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
@@ -50161,47 +50233,96 @@ function truncate6(text, max) {
|
|
|
50161
50233
|
|
|
50162
50234
|
// permission-rule.ts
|
|
50163
50235
|
import { basename as basename6 } from "node:path";
|
|
50164
|
-
|
|
50236
|
+
var FILE_TOOLS2 = new Set([
|
|
50237
|
+
"Edit",
|
|
50238
|
+
"Write",
|
|
50239
|
+
"MultiEdit",
|
|
50240
|
+
"NotebookEdit",
|
|
50241
|
+
"Read"
|
|
50242
|
+
]);
|
|
50243
|
+
var BROAD_ONLY_TOOLS2 = new Set([
|
|
50244
|
+
"Glob",
|
|
50245
|
+
"Grep",
|
|
50246
|
+
"WebFetch",
|
|
50247
|
+
"WebSearch",
|
|
50248
|
+
"Task",
|
|
50249
|
+
"Agent",
|
|
50250
|
+
"TodoWrite",
|
|
50251
|
+
"ExitPlanMode"
|
|
50252
|
+
]);
|
|
50253
|
+
function resolveScopedAllowChoices(toolName, inputPreview) {
|
|
50165
50254
|
if (!toolName)
|
|
50166
50255
|
return null;
|
|
50167
50256
|
const input = parseInput2(inputPreview);
|
|
50168
|
-
|
|
50169
|
-
|
|
50170
|
-
|
|
50171
|
-
|
|
50172
|
-
|
|
50173
|
-
|
|
50174
|
-
|
|
50175
|
-
|
|
50176
|
-
|
|
50257
|
+
if (toolName === "Skill") {
|
|
50258
|
+
const skill = input ? resolveSkillName2(input) : null;
|
|
50259
|
+
if (!skill)
|
|
50260
|
+
return null;
|
|
50261
|
+
if (!/^[A-Za-z0-9._\-+]+$/.test(skill))
|
|
50262
|
+
return null;
|
|
50263
|
+
return {
|
|
50264
|
+
specific: { rule: `Skill(${skill})`, buttonLabel: "This skill", broad: false },
|
|
50265
|
+
broad: { rule: "Skill", buttonLabel: "Any skill", broad: true }
|
|
50266
|
+
};
|
|
50267
|
+
}
|
|
50268
|
+
if (FILE_TOOLS2.has(toolName)) {
|
|
50269
|
+
const path = filePathFrom(input);
|
|
50270
|
+
const broad = { rule: toolName, buttonLabel: "Any file", broad: true };
|
|
50271
|
+
if (path) {
|
|
50177
50272
|
return {
|
|
50178
|
-
rule:
|
|
50179
|
-
|
|
50273
|
+
specific: { rule: `${toolName}(${path})`, buttonLabel: "This file", broad: false },
|
|
50274
|
+
broad
|
|
50180
50275
|
};
|
|
50181
50276
|
}
|
|
50182
|
-
|
|
50183
|
-
|
|
50184
|
-
|
|
50185
|
-
|
|
50186
|
-
|
|
50187
|
-
|
|
50188
|
-
|
|
50189
|
-
|
|
50190
|
-
|
|
50191
|
-
|
|
50192
|
-
|
|
50193
|
-
case "Agent":
|
|
50194
|
-
case "TodoWrite":
|
|
50195
|
-
case "ExitPlanMode": {
|
|
50196
|
-
return { rule: toolName, label: toolName };
|
|
50197
|
-
}
|
|
50198
|
-
default: {
|
|
50199
|
-
if (/^mcp__[A-Za-z0-9_\-]+(__[A-Za-z0-9_\-]+)?$/.test(toolName)) {
|
|
50200
|
-
return { rule: toolName, label: toolName };
|
|
50201
|
-
}
|
|
50202
|
-
return null;
|
|
50277
|
+
return { broad };
|
|
50278
|
+
}
|
|
50279
|
+
if (toolName === "Bash") {
|
|
50280
|
+
const broad = { rule: "Bash", buttonLabel: "Any command", broad: true };
|
|
50281
|
+
const cmd = input ? readString2(input, "command") : null;
|
|
50282
|
+
const tok = cmd ? bashFirstToken(cmd) : null;
|
|
50283
|
+
if (tok) {
|
|
50284
|
+
return {
|
|
50285
|
+
specific: { rule: `Bash(${tok}:*)`, buttonLabel: `${tok} commands`, broad: false },
|
|
50286
|
+
broad
|
|
50287
|
+
};
|
|
50203
50288
|
}
|
|
50289
|
+
return { broad };
|
|
50290
|
+
}
|
|
50291
|
+
if (BROAD_ONLY_TOOLS2.has(toolName)) {
|
|
50292
|
+
return { broad: { rule: toolName, buttonLabel: "Always allow", broad: true } };
|
|
50204
50293
|
}
|
|
50294
|
+
const mcp = /^mcp__([A-Za-z0-9_-]+)__([A-Za-z0-9_-]+)$/.exec(toolName);
|
|
50295
|
+
if (mcp) {
|
|
50296
|
+
const server = mcp[1];
|
|
50297
|
+
return {
|
|
50298
|
+
specific: { rule: toolName, buttonLabel: "This action", broad: false },
|
|
50299
|
+
broad: { rule: `mcp__${server}__*`, buttonLabel: `All ${prettyMcpServer2(server)}`, broad: true }
|
|
50300
|
+
};
|
|
50301
|
+
}
|
|
50302
|
+
if (/^mcp__[A-Za-z0-9_-]+$/.test(toolName)) {
|
|
50303
|
+
return { broad: { rule: toolName, buttonLabel: "Always allow", broad: true } };
|
|
50304
|
+
}
|
|
50305
|
+
return null;
|
|
50306
|
+
}
|
|
50307
|
+
function prettyMcpServer2(server) {
|
|
50308
|
+
return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
50309
|
+
}
|
|
50310
|
+
function resolveSkillName2(input) {
|
|
50311
|
+
return readString2(input, "skill") ?? readString2(input, "skill_name") ?? readString2(input, "skillName") ?? readString2(input, "name") ?? skillBasenameFromPath2(input);
|
|
50312
|
+
}
|
|
50313
|
+
function filePathFrom(input) {
|
|
50314
|
+
if (!input)
|
|
50315
|
+
return null;
|
|
50316
|
+
return readString2(input, "file_path") ?? readString2(input, "notebook_path");
|
|
50317
|
+
}
|
|
50318
|
+
function bashFirstToken(command) {
|
|
50319
|
+
const m = /^\s*([^\s|&;<>()`$]+)/.exec(command);
|
|
50320
|
+
if (!m)
|
|
50321
|
+
return null;
|
|
50322
|
+
const tok = m[1];
|
|
50323
|
+
if (tok.includes(".."))
|
|
50324
|
+
return null;
|
|
50325
|
+
return /^[A-Za-z0-9._\-\/]+$/.test(tok) ? tok : null;
|
|
50205
50326
|
}
|
|
50206
50327
|
function parseInput2(raw) {
|
|
50207
50328
|
if (!raw || typeof raw !== "string")
|
|
@@ -50792,11 +50913,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
50792
50913
|
}
|
|
50793
50914
|
|
|
50794
50915
|
// ../src/build-info.ts
|
|
50795
|
-
var VERSION = "0.14.
|
|
50796
|
-
var COMMIT_SHA = "
|
|
50797
|
-
var COMMIT_DATE = "2026-05-
|
|
50798
|
-
var LATEST_PR =
|
|
50799
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
50916
|
+
var VERSION = "0.14.13";
|
|
50917
|
+
var COMMIT_SHA = "240594e9";
|
|
50918
|
+
var COMMIT_DATE = "2026-05-29T12:19:57Z";
|
|
50919
|
+
var LATEST_PR = 1996;
|
|
50920
|
+
var COMMITS_AHEAD_OF_TAG = 0;
|
|
50800
50921
|
|
|
50801
50922
|
// gateway/boot-version.ts
|
|
50802
50923
|
function formatRelativeAgo(iso) {
|
|
@@ -52795,6 +52916,12 @@ if (inboundSpool != null) {
|
|
|
52795
52916
|
}
|
|
52796
52917
|
}
|
|
52797
52918
|
var pendingPermissionBuffer = createPendingPermissionBuffer();
|
|
52919
|
+
function buildPermissionActionRow(requestId, showAlways) {
|
|
52920
|
+
const kb = new import_grammy9.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow once", `perm:allow:${requestId}`);
|
|
52921
|
+
if (showAlways)
|
|
52922
|
+
kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
|
|
52923
|
+
return kb;
|
|
52924
|
+
}
|
|
52798
52925
|
function dispatchPermissionVerdict(ev) {
|
|
52799
52926
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
52800
52927
|
const delivered = ipcServer.sendToAgent(selfAgent, ev);
|
|
@@ -53013,11 +53140,8 @@ var ipcServer = createIpcServer({
|
|
|
53013
53140
|
description,
|
|
53014
53141
|
agentName: _client.agentName
|
|
53015
53142
|
});
|
|
53016
|
-
const
|
|
53017
|
-
const keyboard =
|
|
53018
|
-
if (alwaysRule != null) {
|
|
53019
|
-
keyboard.row().text(`\uD83D\uDD01 Always allow ${alwaysRule.label}`, `perm:always:${requestId}`);
|
|
53020
|
-
}
|
|
53143
|
+
const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
|
|
53144
|
+
const keyboard = buildPermissionActionRow(requestId, showAlways);
|
|
53021
53145
|
const activeTurn = currentTurn;
|
|
53022
53146
|
const permTopic = resolveAgentOutboundTopic({
|
|
53023
53147
|
kind: "permission",
|
|
@@ -59715,7 +59839,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
59715
59839
|
}
|
|
59716
59840
|
return;
|
|
59717
59841
|
}
|
|
59718
|
-
const m = /^perm:(allow|deny|
|
|
59842
|
+
const m = /^perm:(allow|deny|always|asn|asb|back):([a-km-z]{5})$/.exec(data);
|
|
59719
59843
|
if (!m) {
|
|
59720
59844
|
await ctx.answerCallbackQuery().catch(() => {});
|
|
59721
59845
|
return;
|
|
@@ -59727,45 +59851,43 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
59727
59851
|
return;
|
|
59728
59852
|
}
|
|
59729
59853
|
const [, behavior, request_id] = m;
|
|
59730
|
-
if (behavior === "
|
|
59854
|
+
if (behavior === "always" || behavior === "back") {
|
|
59731
59855
|
const details = pendingPermissions.get(request_id);
|
|
59732
59856
|
if (!details) {
|
|
59733
59857
|
await ctx.answerCallbackQuery({ text: "Details no longer available." }).catch(() => {});
|
|
59734
59858
|
return;
|
|
59735
59859
|
}
|
|
59736
|
-
|
|
59737
|
-
|
|
59738
|
-
|
|
59739
|
-
|
|
59740
|
-
|
|
59741
|
-
|
|
59742
|
-
|
|
59743
|
-
|
|
59744
|
-
|
|
59745
|
-
|
|
59746
|
-
|
|
59747
|
-
|
|
59748
|
-
|
|
59749
|
-
const expandedRule = resolveAlwaysAllowRule(tool_name, input_preview);
|
|
59750
|
-
const expandedKeyboard = new import_grammy9.InlineKeyboard().text("\u2705 Allow", `perm:allow:${request_id}`).text("\u274C Deny", `perm:deny:${request_id}`);
|
|
59751
|
-
if (expandedRule != null) {
|
|
59752
|
-
expandedKeyboard.row().text(`\uD83D\uDD01 Always allow ${expandedRule.label}`, `perm:always:${request_id}`);
|
|
59860
|
+
let keyboard;
|
|
59861
|
+
if (behavior === "back") {
|
|
59862
|
+
keyboard = buildPermissionActionRow(request_id, true);
|
|
59863
|
+
} else {
|
|
59864
|
+
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
59865
|
+
if (choices == null) {
|
|
59866
|
+
await ctx.answerCallbackQuery({ text: "No always-allow rule for this tool." }).catch(() => {});
|
|
59867
|
+
return;
|
|
59868
|
+
}
|
|
59869
|
+
keyboard = new import_grammy9.InlineKeyboard().text("\u2190 Back", `perm:back:${request_id}`);
|
|
59870
|
+
if (choices.specific)
|
|
59871
|
+
keyboard.text(choices.specific.buttonLabel, `perm:asn:${request_id}`);
|
|
59872
|
+
keyboard.text(`${choices.broad.buttonLabel} \u26A0\uFE0F`, `perm:asb:${request_id}`);
|
|
59753
59873
|
}
|
|
59754
|
-
await ctx.
|
|
59874
|
+
await ctx.editMessageReplyMarkup({ reply_markup: keyboard }).catch(() => {});
|
|
59755
59875
|
await ctx.answerCallbackQuery().catch(() => {});
|
|
59756
59876
|
return;
|
|
59757
59877
|
}
|
|
59758
|
-
if (behavior === "
|
|
59878
|
+
if (behavior === "asn" || behavior === "asb") {
|
|
59759
59879
|
const details = pendingPermissions.get(request_id);
|
|
59760
59880
|
if (!details) {
|
|
59761
59881
|
await ctx.answerCallbackQuery({ text: "Details no longer available." }).catch(() => {});
|
|
59762
59882
|
return;
|
|
59763
59883
|
}
|
|
59764
|
-
const
|
|
59765
|
-
if (
|
|
59884
|
+
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
59885
|
+
if (choices == null) {
|
|
59766
59886
|
await ctx.answerCallbackQuery({ text: "Cannot synthesize an always-allow rule for this tool." }).catch(() => {});
|
|
59767
59887
|
return;
|
|
59768
59888
|
}
|
|
59889
|
+
const chosen = behavior === "asn" ? choices.specific ?? choices.broad : choices.broad;
|
|
59890
|
+
const grantPhrase = describeGrant(details.tool_name, details.input_preview, chosen);
|
|
59769
59891
|
const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
|
|
59770
59892
|
if (!agentName3) {
|
|
59771
59893
|
await ctx.answerCallbackQuery({ text: "Always-allow needs SWITCHROOM_AGENT_NAME \u2014 gateway is misconfigured." }).catch(() => {});
|
|
@@ -59776,7 +59898,7 @@ ${prettyInput}`;
|
|
|
59776
59898
|
type: "permission",
|
|
59777
59899
|
requestId: request_id,
|
|
59778
59900
|
behavior: "allow",
|
|
59779
|
-
rule:
|
|
59901
|
+
rule: chosen.rule
|
|
59780
59902
|
});
|
|
59781
59903
|
let durable = false;
|
|
59782
59904
|
let legacy = false;
|
|
@@ -59787,26 +59909,26 @@ ${prettyInput}`;
|
|
|
59787
59909
|
try {
|
|
59788
59910
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
59789
59911
|
const raw = readFileSync36(cfgPath, "utf8");
|
|
59790
|
-
return synthesizeAllowRuleDiff({ agentName: agentName3, rule:
|
|
59912
|
+
return synthesizeAllowRuleDiff({ agentName: agentName3, rule: chosen.rule, configText: raw });
|
|
59791
59913
|
} catch (err) {
|
|
59792
59914
|
process.stderr.write(`telegram gateway: always-allow diff synth failed: ${err.message}
|
|
59793
59915
|
`);
|
|
59794
59916
|
return null;
|
|
59795
59917
|
}
|
|
59796
59918
|
})();
|
|
59797
|
-
const correlationKey = `${agentName3}::${
|
|
59919
|
+
const correlationKey = `${agentName3}::${chosen.rule}`;
|
|
59798
59920
|
try {
|
|
59799
59921
|
if (unifiedDiff == null) {
|
|
59800
59922
|
legacy = true;
|
|
59801
59923
|
} else {
|
|
59802
|
-
pendingAlwaysAllowCorrelations.set(correlationKey, { agentName: agentName3, rule:
|
|
59924
|
+
pendingAlwaysAllowCorrelations.set(correlationKey, { agentName: agentName3, rule: chosen.rule, unifiedDiff, createdAt: Date.now() });
|
|
59803
59925
|
const req = {
|
|
59804
59926
|
v: 1,
|
|
59805
59927
|
op: "config_propose_edit",
|
|
59806
59928
|
request_id: hostdRequestId("gw-always-allow"),
|
|
59807
59929
|
args: {
|
|
59808
59930
|
unified_diff: unifiedDiff,
|
|
59809
|
-
reason: `Operator 'always allow'
|
|
59931
|
+
reason: `Operator 'always allow': ${agentName3} can ${grantPhrase}`,
|
|
59810
59932
|
target_path: "/state/config/switchroom.yaml"
|
|
59811
59933
|
}
|
|
59812
59934
|
};
|
|
@@ -59816,7 +59938,7 @@ ${prettyInput}`;
|
|
|
59816
59938
|
legacy = true;
|
|
59817
59939
|
} else if (resp.result === "completed") {
|
|
59818
59940
|
durable = true;
|
|
59819
|
-
process.stderr.write(`telegram gateway: always-allow durable via hostd rule="${
|
|
59941
|
+
process.stderr.write(`telegram gateway: always-allow durable via hostd rule="${chosen.rule}" agent=${agentName3} (request_id=${request_id})
|
|
59820
59942
|
`);
|
|
59821
59943
|
} else {
|
|
59822
59944
|
failReason = resp.error ?? `hostd ${resp.result}`;
|
|
@@ -59828,19 +59950,19 @@ ${prettyInput}`;
|
|
|
59828
59950
|
}
|
|
59829
59951
|
if (legacy) {
|
|
59830
59952
|
try {
|
|
59831
|
-
switchroomExec(["agent", "grant", agentName3,
|
|
59953
|
+
switchroomExec(["agent", "grant", agentName3, chosen.rule, "--no-restart"]);
|
|
59832
59954
|
try {
|
|
59833
59955
|
const cfg = loadConfig2();
|
|
59834
59956
|
const rawAgent = cfg.agents?.[agentName3];
|
|
59835
59957
|
if (rawAgent) {
|
|
59836
59958
|
const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
|
|
59837
59959
|
const allowList = resolved.tools?.allow ?? [];
|
|
59838
|
-
if (isRulePersisted(allowList,
|
|
59960
|
+
if (isRulePersisted(allowList, chosen.rule)) {
|
|
59839
59961
|
durable = true;
|
|
59840
|
-
process.stderr.write(`telegram gateway: always-allow added rule="${
|
|
59962
|
+
process.stderr.write(`telegram gateway: always-allow added rule="${chosen.rule}" agent=${agentName3} via legacy grant (request_id=${request_id})
|
|
59841
59963
|
`);
|
|
59842
59964
|
} else {
|
|
59843
|
-
failReason = `rule "${
|
|
59965
|
+
failReason = `rule "${chosen.rule}" not found in resolved tools.allow after write \u2014 config location may have drifted`;
|
|
59844
59966
|
process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${failReason} (request_id=${request_id})
|
|
59845
59967
|
`);
|
|
59846
59968
|
}
|
|
@@ -59865,10 +59987,10 @@ ${prettyInput}`;
|
|
|
59865
59987
|
}
|
|
59866
59988
|
const ok = durable;
|
|
59867
59989
|
const legacyNote = legacy && durable;
|
|
59868
|
-
const ackText = ok ? legacyNote ? `\
|
|
59990
|
+
const ackText = ok ? legacyNote ? `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking (legacy path).` : `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking.` : editLockHint ? `\u26A0\uFE0F Allowed for now \u2014 config edits are locked. Enable hostd.config_edit_enabled.` : `\u26A0\uFE0F Allowed for now, but "always" did NOT save \u2014 it will ask again after restart. Check gateway log.`;
|
|
59869
59991
|
const sourceMsg = ctx.callbackQuery?.message;
|
|
59870
59992
|
const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
|
|
59871
|
-
const editLabel = ok ? legacyNote ? `\
|
|
59993
|
+
const editLabel = ok ? legacyNote ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking (legacy path); restart agent for full effect` : `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking; restart agent for full effect` : editLockHint ? `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> Config edits are locked; enable <code>hostd.config_edit_enabled</code>.` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
|
|
59872
59994
|
await finalizeCallback(ctx, {
|
|
59873
59995
|
ackText: ackText.slice(0, 200),
|
|
59874
59996
|
newText: baseText2 ? `${baseText2}
|