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