switchroom 0.14.11 → 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/agent-scheduler/index.js +4 -0
- package/dist/auth-broker/index.js +4 -0
- package/dist/cli/notion-write-pretool.mjs +4 -0
- package/dist/cli/switchroom.js +255 -12
- package/dist/host-control/main.js +84 -6
- package/dist/vault/approvals/kernel-server.js +5 -1
- package/dist/vault/broker/server.js +5 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +61 -8
- package/telegram-plugin/dist/gateway/gateway.js +287 -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
|
@@ -23607,7 +23607,7 @@ var init_dist = __esm(() => {
|
|
|
23607
23607
|
});
|
|
23608
23608
|
|
|
23609
23609
|
// ../src/config/schema.ts
|
|
23610
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
|
|
23610
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
|
|
23611
23611
|
var init_schema = __esm(() => {
|
|
23612
23612
|
init_zod();
|
|
23613
23613
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -24057,6 +24057,9 @@ var init_schema = __esm(() => {
|
|
|
24057
24057
|
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
|
|
24058
24058
|
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
24059
24059
|
});
|
|
24060
|
+
WebServiceConfigSchema = exports_external.object({
|
|
24061
|
+
managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false \u2014 existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
|
|
24062
|
+
});
|
|
24060
24063
|
HostdConfigSchema = exports_external.object({
|
|
24061
24064
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
|
24062
24065
|
config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit \u00a75). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
|
|
@@ -24090,6 +24093,7 @@ var init_schema = __esm(() => {
|
|
|
24090
24093
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
24091
24094
|
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
24092
24095
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a \u2014 disabled by default)."),
|
|
24096
|
+
web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
|
|
24093
24097
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
24094
24098
|
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
24095
24099
|
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
@@ -50000,10 +50004,32 @@ function defaultReadEvents(stateDir) {
|
|
|
50000
50004
|
}
|
|
50001
50005
|
// permission-title.ts
|
|
50002
50006
|
import { basename as basename5 } from "node:path";
|
|
50003
|
-
|
|
50004
|
-
|
|
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;
|
|
50005
50032
|
var DESCRIPTION_LINE_MAX = 240;
|
|
50006
|
-
var INPUT_VALUE_MAX = 60;
|
|
50007
50033
|
var MCP_TOOL_DESCRIPTIONS = {
|
|
50008
50034
|
"mcp__agent-config__config_get": "Read its own merged config",
|
|
50009
50035
|
"mcp__agent-config__cron_list": "List its own scheduled tasks",
|
|
@@ -50023,103 +50049,153 @@ var MCP_TOOL_DESCRIPTIONS = {
|
|
|
50023
50049
|
mcp__hostd__update_apply: "Apply a fleet-wide update (pull + recreate)",
|
|
50024
50050
|
mcp__hindsight__recall: "Recall relevant memories",
|
|
50025
50051
|
mcp__hindsight__retain: "Retain a memory",
|
|
50026
|
-
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"
|
|
50027
50055
|
};
|
|
50028
|
-
|
|
50029
|
-
|
|
50030
|
-
|
|
50031
|
-
|
|
50032
|
-
|
|
50033
|
-
|
|
50034
|
-
|
|
50035
|
-
|
|
50036
|
-
|
|
50037
|
-
|
|
50038
|
-
|
|
50039
|
-
|
|
50040
|
-
|
|
50041
|
-
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))}`);
|
|
50042
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) {
|
|
50043
50077
|
const input = parseInput(inputPreview);
|
|
50044
|
-
if (
|
|
50045
|
-
return toolName;
|
|
50078
|
+
if (toolName.startsWith("mcp__"))
|
|
50079
|
+
return naturalMcpAction(toolName, input);
|
|
50046
50080
|
switch (toolName) {
|
|
50047
|
-
case "Skill": {
|
|
50048
|
-
const skill = readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
|
|
50049
|
-
if (skill)
|
|
50050
|
-
return `${toolName} (${skill})`;
|
|
50051
|
-
const command = readString(input, "command");
|
|
50052
|
-
if (command)
|
|
50053
|
-
return `${toolName}: ${truncate6(command, COMMAND_TITLE_MAX)}`;
|
|
50054
|
-
const argHint = firstScalarArgHint(input);
|
|
50055
|
-
return argHint ? `${toolName} (${argHint})` : toolName;
|
|
50056
|
-
}
|
|
50057
|
-
case "Bash": {
|
|
50058
|
-
const command = readString(input, "command");
|
|
50059
|
-
return command ? `${toolName}: ${truncate6(command, COMMAND_TITLE_MAX)}` : toolName;
|
|
50060
|
-
}
|
|
50061
|
-
case "Read":
|
|
50062
50081
|
case "Edit":
|
|
50063
|
-
case "Write":
|
|
50064
50082
|
case "MultiEdit":
|
|
50065
50083
|
case "NotebookEdit": {
|
|
50066
|
-
const
|
|
50067
|
-
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";
|
|
50068
50102
|
}
|
|
50069
50103
|
case "Glob":
|
|
50070
50104
|
case "Grep": {
|
|
50071
|
-
const
|
|
50072
|
-
return
|
|
50105
|
+
const p = input ? readString(input, "pattern") : null;
|
|
50106
|
+
return p ? `search files for: ${truncate6(p, COMMAND_TITLE_MAX)}` : "search files";
|
|
50073
50107
|
}
|
|
50074
|
-
case "WebFetch":
|
|
50075
50108
|
case "WebSearch": {
|
|
50076
|
-
const
|
|
50077
|
-
return
|
|
50109
|
+
const q = input ? readString(input, "query") : null;
|
|
50110
|
+
return q ? `search the web for: ${truncate6(q, COMMAND_TITLE_MAX)}` : "search the web";
|
|
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";
|
|
50078
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";
|
|
50079
50123
|
default:
|
|
50080
|
-
return toolName
|
|
50081
|
-
}
|
|
50082
|
-
}
|
|
50083
|
-
function
|
|
50084
|
-
const
|
|
50085
|
-
const
|
|
50086
|
-
const
|
|
50087
|
-
|
|
50088
|
-
|
|
50089
|
-
|
|
50090
|
-
|
|
50091
|
-
|
|
50092
|
-
|
|
50093
|
-
|
|
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);
|
|
50094
50180
|
}
|
|
50095
|
-
return lines.join(`
|
|
50096
|
-
`);
|
|
50097
50181
|
}
|
|
50098
|
-
function
|
|
50099
|
-
return
|
|
50182
|
+
function resolveSkillName(input) {
|
|
50183
|
+
return readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
|
|
50100
50184
|
}
|
|
50101
|
-
function
|
|
50185
|
+
function fileBase(input) {
|
|
50102
50186
|
if (!input)
|
|
50103
50187
|
return null;
|
|
50104
|
-
const
|
|
50105
|
-
|
|
50106
|
-
|
|
50107
|
-
|
|
50108
|
-
|
|
50109
|
-
|
|
50110
|
-
|
|
50111
|
-
|
|
50112
|
-
|
|
50113
|
-
|
|
50114
|
-
|
|
50115
|
-
if (typeof value === "string" && value.length > 0) {
|
|
50116
|
-
return `${key}: ${truncate6(value, INPUT_VALUE_MAX)}`;
|
|
50117
|
-
}
|
|
50118
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
50119
|
-
return `${key}: ${String(value)}`;
|
|
50120
|
-
}
|
|
50121
|
-
}
|
|
50122
|
-
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, ">");
|
|
50123
50199
|
}
|
|
50124
50200
|
function parseInput(raw) {
|
|
50125
50201
|
if (!raw || typeof raw !== "string")
|
|
@@ -50145,8 +50221,8 @@ function skillBasenameFromPath(input) {
|
|
|
50145
50221
|
return null;
|
|
50146
50222
|
const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
50147
50223
|
const lastSlash = trimmed.lastIndexOf("/");
|
|
50148
|
-
const
|
|
50149
|
-
return
|
|
50224
|
+
const base = lastSlash >= 0 ? trimmed.slice(lastSlash + 1) : trimmed;
|
|
50225
|
+
return base.length > 0 ? base : null;
|
|
50150
50226
|
}
|
|
50151
50227
|
function truncate6(text, max) {
|
|
50152
50228
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
@@ -50157,47 +50233,96 @@ function truncate6(text, max) {
|
|
|
50157
50233
|
|
|
50158
50234
|
// permission-rule.ts
|
|
50159
50235
|
import { basename as basename6 } from "node:path";
|
|
50160
|
-
|
|
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) {
|
|
50161
50254
|
if (!toolName)
|
|
50162
50255
|
return null;
|
|
50163
50256
|
const input = parseInput2(inputPreview);
|
|
50164
|
-
|
|
50165
|
-
|
|
50166
|
-
|
|
50167
|
-
|
|
50168
|
-
|
|
50169
|
-
|
|
50170
|
-
|
|
50171
|
-
|
|
50172
|
-
|
|
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) {
|
|
50173
50272
|
return {
|
|
50174
|
-
rule:
|
|
50175
|
-
|
|
50273
|
+
specific: { rule: `${toolName}(${path})`, buttonLabel: "This file", broad: false },
|
|
50274
|
+
broad
|
|
50176
50275
|
};
|
|
50177
50276
|
}
|
|
50178
|
-
|
|
50179
|
-
|
|
50180
|
-
|
|
50181
|
-
|
|
50182
|
-
|
|
50183
|
-
|
|
50184
|
-
|
|
50185
|
-
|
|
50186
|
-
|
|
50187
|
-
|
|
50188
|
-
|
|
50189
|
-
case "Agent":
|
|
50190
|
-
case "TodoWrite":
|
|
50191
|
-
case "ExitPlanMode": {
|
|
50192
|
-
return { rule: toolName, label: toolName };
|
|
50193
|
-
}
|
|
50194
|
-
default: {
|
|
50195
|
-
if (/^mcp__[A-Za-z0-9_\-]+(__[A-Za-z0-9_\-]+)?$/.test(toolName)) {
|
|
50196
|
-
return { rule: toolName, label: toolName };
|
|
50197
|
-
}
|
|
50198
|
-
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
|
+
};
|
|
50199
50288
|
}
|
|
50289
|
+
return { broad };
|
|
50290
|
+
}
|
|
50291
|
+
if (BROAD_ONLY_TOOLS2.has(toolName)) {
|
|
50292
|
+
return { broad: { rule: toolName, buttonLabel: "Always allow", broad: true } };
|
|
50200
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;
|
|
50201
50326
|
}
|
|
50202
50327
|
function parseInput2(raw) {
|
|
50203
50328
|
if (!raw || typeof raw !== "string")
|
|
@@ -50788,10 +50913,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
50788
50913
|
}
|
|
50789
50914
|
|
|
50790
50915
|
// ../src/build-info.ts
|
|
50791
|
-
var VERSION = "0.14.
|
|
50792
|
-
var COMMIT_SHA = "
|
|
50793
|
-
var COMMIT_DATE = "2026-05-
|
|
50794
|
-
var LATEST_PR =
|
|
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;
|
|
50795
50920
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
50796
50921
|
|
|
50797
50922
|
// gateway/boot-version.ts
|
|
@@ -52791,6 +52916,12 @@ if (inboundSpool != null) {
|
|
|
52791
52916
|
}
|
|
52792
52917
|
}
|
|
52793
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
|
+
}
|
|
52794
52925
|
function dispatchPermissionVerdict(ev) {
|
|
52795
52926
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
52796
52927
|
const delivered = ipcServer.sendToAgent(selfAgent, ev);
|
|
@@ -53009,11 +53140,8 @@ var ipcServer = createIpcServer({
|
|
|
53009
53140
|
description,
|
|
53010
53141
|
agentName: _client.agentName
|
|
53011
53142
|
});
|
|
53012
|
-
const
|
|
53013
|
-
const keyboard =
|
|
53014
|
-
if (alwaysRule != null) {
|
|
53015
|
-
keyboard.row().text(`\uD83D\uDD01 Always allow ${alwaysRule.label}`, `perm:always:${requestId}`);
|
|
53016
|
-
}
|
|
53143
|
+
const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
|
|
53144
|
+
const keyboard = buildPermissionActionRow(requestId, showAlways);
|
|
53017
53145
|
const activeTurn = currentTurn;
|
|
53018
53146
|
const permTopic = resolveAgentOutboundTopic({
|
|
53019
53147
|
kind: "permission",
|
|
@@ -59711,7 +59839,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
59711
59839
|
}
|
|
59712
59840
|
return;
|
|
59713
59841
|
}
|
|
59714
|
-
const m = /^perm:(allow|deny|
|
|
59842
|
+
const m = /^perm:(allow|deny|always|asn|asb|back):([a-km-z]{5})$/.exec(data);
|
|
59715
59843
|
if (!m) {
|
|
59716
59844
|
await ctx.answerCallbackQuery().catch(() => {});
|
|
59717
59845
|
return;
|
|
@@ -59723,45 +59851,43 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
59723
59851
|
return;
|
|
59724
59852
|
}
|
|
59725
59853
|
const [, behavior, request_id] = m;
|
|
59726
|
-
if (behavior === "
|
|
59854
|
+
if (behavior === "always" || behavior === "back") {
|
|
59727
59855
|
const details = pendingPermissions.get(request_id);
|
|
59728
59856
|
if (!details) {
|
|
59729
59857
|
await ctx.answerCallbackQuery({ text: "Details no longer available." }).catch(() => {});
|
|
59730
59858
|
return;
|
|
59731
59859
|
}
|
|
59732
|
-
|
|
59733
|
-
|
|
59734
|
-
|
|
59735
|
-
|
|
59736
|
-
|
|
59737
|
-
|
|
59738
|
-
|
|
59739
|
-
|
|
59740
|
-
|
|
59741
|
-
|
|
59742
|
-
|
|
59743
|
-
|
|
59744
|
-
|
|
59745
|
-
const expandedRule = resolveAlwaysAllowRule(tool_name, input_preview);
|
|
59746
|
-
const expandedKeyboard = new import_grammy9.InlineKeyboard().text("\u2705 Allow", `perm:allow:${request_id}`).text("\u274C Deny", `perm:deny:${request_id}`);
|
|
59747
|
-
if (expandedRule != null) {
|
|
59748
|
-
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}`);
|
|
59749
59873
|
}
|
|
59750
|
-
await ctx.
|
|
59874
|
+
await ctx.editMessageReplyMarkup({ reply_markup: keyboard }).catch(() => {});
|
|
59751
59875
|
await ctx.answerCallbackQuery().catch(() => {});
|
|
59752
59876
|
return;
|
|
59753
59877
|
}
|
|
59754
|
-
if (behavior === "
|
|
59878
|
+
if (behavior === "asn" || behavior === "asb") {
|
|
59755
59879
|
const details = pendingPermissions.get(request_id);
|
|
59756
59880
|
if (!details) {
|
|
59757
59881
|
await ctx.answerCallbackQuery({ text: "Details no longer available." }).catch(() => {});
|
|
59758
59882
|
return;
|
|
59759
59883
|
}
|
|
59760
|
-
const
|
|
59761
|
-
if (
|
|
59884
|
+
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
59885
|
+
if (choices == null) {
|
|
59762
59886
|
await ctx.answerCallbackQuery({ text: "Cannot synthesize an always-allow rule for this tool." }).catch(() => {});
|
|
59763
59887
|
return;
|
|
59764
59888
|
}
|
|
59889
|
+
const chosen = behavior === "asn" ? choices.specific ?? choices.broad : choices.broad;
|
|
59890
|
+
const grantPhrase = describeGrant(details.tool_name, details.input_preview, chosen);
|
|
59765
59891
|
const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
|
|
59766
59892
|
if (!agentName3) {
|
|
59767
59893
|
await ctx.answerCallbackQuery({ text: "Always-allow needs SWITCHROOM_AGENT_NAME \u2014 gateway is misconfigured." }).catch(() => {});
|
|
@@ -59772,7 +59898,7 @@ ${prettyInput}`;
|
|
|
59772
59898
|
type: "permission",
|
|
59773
59899
|
requestId: request_id,
|
|
59774
59900
|
behavior: "allow",
|
|
59775
|
-
rule:
|
|
59901
|
+
rule: chosen.rule
|
|
59776
59902
|
});
|
|
59777
59903
|
let durable = false;
|
|
59778
59904
|
let legacy = false;
|
|
@@ -59783,26 +59909,26 @@ ${prettyInput}`;
|
|
|
59783
59909
|
try {
|
|
59784
59910
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
59785
59911
|
const raw = readFileSync36(cfgPath, "utf8");
|
|
59786
|
-
return synthesizeAllowRuleDiff({ agentName: agentName3, rule:
|
|
59912
|
+
return synthesizeAllowRuleDiff({ agentName: agentName3, rule: chosen.rule, configText: raw });
|
|
59787
59913
|
} catch (err) {
|
|
59788
59914
|
process.stderr.write(`telegram gateway: always-allow diff synth failed: ${err.message}
|
|
59789
59915
|
`);
|
|
59790
59916
|
return null;
|
|
59791
59917
|
}
|
|
59792
59918
|
})();
|
|
59793
|
-
const correlationKey = `${agentName3}::${
|
|
59919
|
+
const correlationKey = `${agentName3}::${chosen.rule}`;
|
|
59794
59920
|
try {
|
|
59795
59921
|
if (unifiedDiff == null) {
|
|
59796
59922
|
legacy = true;
|
|
59797
59923
|
} else {
|
|
59798
|
-
pendingAlwaysAllowCorrelations.set(correlationKey, { agentName: agentName3, rule:
|
|
59924
|
+
pendingAlwaysAllowCorrelations.set(correlationKey, { agentName: agentName3, rule: chosen.rule, unifiedDiff, createdAt: Date.now() });
|
|
59799
59925
|
const req = {
|
|
59800
59926
|
v: 1,
|
|
59801
59927
|
op: "config_propose_edit",
|
|
59802
59928
|
request_id: hostdRequestId("gw-always-allow"),
|
|
59803
59929
|
args: {
|
|
59804
59930
|
unified_diff: unifiedDiff,
|
|
59805
|
-
reason: `Operator 'always allow'
|
|
59931
|
+
reason: `Operator 'always allow': ${agentName3} can ${grantPhrase}`,
|
|
59806
59932
|
target_path: "/state/config/switchroom.yaml"
|
|
59807
59933
|
}
|
|
59808
59934
|
};
|
|
@@ -59812,7 +59938,7 @@ ${prettyInput}`;
|
|
|
59812
59938
|
legacy = true;
|
|
59813
59939
|
} else if (resp.result === "completed") {
|
|
59814
59940
|
durable = true;
|
|
59815
|
-
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})
|
|
59816
59942
|
`);
|
|
59817
59943
|
} else {
|
|
59818
59944
|
failReason = resp.error ?? `hostd ${resp.result}`;
|
|
@@ -59824,19 +59950,19 @@ ${prettyInput}`;
|
|
|
59824
59950
|
}
|
|
59825
59951
|
if (legacy) {
|
|
59826
59952
|
try {
|
|
59827
|
-
switchroomExec(["agent", "grant", agentName3,
|
|
59953
|
+
switchroomExec(["agent", "grant", agentName3, chosen.rule, "--no-restart"]);
|
|
59828
59954
|
try {
|
|
59829
59955
|
const cfg = loadConfig2();
|
|
59830
59956
|
const rawAgent = cfg.agents?.[agentName3];
|
|
59831
59957
|
if (rawAgent) {
|
|
59832
59958
|
const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
|
|
59833
59959
|
const allowList = resolved.tools?.allow ?? [];
|
|
59834
|
-
if (isRulePersisted(allowList,
|
|
59960
|
+
if (isRulePersisted(allowList, chosen.rule)) {
|
|
59835
59961
|
durable = true;
|
|
59836
|
-
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})
|
|
59837
59963
|
`);
|
|
59838
59964
|
} else {
|
|
59839
|
-
failReason = `rule "${
|
|
59965
|
+
failReason = `rule "${chosen.rule}" not found in resolved tools.allow after write \u2014 config location may have drifted`;
|
|
59840
59966
|
process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${failReason} (request_id=${request_id})
|
|
59841
59967
|
`);
|
|
59842
59968
|
}
|
|
@@ -59861,10 +59987,10 @@ ${prettyInput}`;
|
|
|
59861
59987
|
}
|
|
59862
59988
|
const ok = durable;
|
|
59863
59989
|
const legacyNote = legacy && durable;
|
|
59864
|
-
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.`;
|
|
59865
59991
|
const sourceMsg = ctx.callbackQuery?.message;
|
|
59866
59992
|
const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
|
|
59867
|
-
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.`;
|
|
59868
59994
|
await finalizeCallback(ctx, {
|
|
59869
59995
|
ackText: ackText.slice(0, 200),
|
|
59870
59996
|
newText: baseText2 ? `${baseText2}
|