switchroom 0.15.12 → 0.15.14
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 +324 -14
- package/dist/auth-broker/index.js +61 -4
- package/dist/cli/notion-write-pretool.mjs +61 -4
- package/dist/cli/switchroom.js +402 -113
- package/dist/host-control/main.js +61 -4
- package/dist/vault/approvals/kernel-server.js +62 -5
- package/dist/vault/broker/server.js +62 -5
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +30 -13
- package/profiles/_shared/agent-self-service.md.hbs +37 -0
- package/telegram-plugin/bridge/bridge.ts +38 -1
- package/telegram-plugin/dist/bridge/bridge.js +31 -1
- package/telegram-plugin/dist/gateway/gateway.js +536 -53
- package/telegram-plugin/dist/server.js +31 -1
- package/telegram-plugin/gateway/gateway.ts +169 -6
- package/telegram-plugin/gateway/ipc-protocol.ts +31 -1
- package/telegram-plugin/gateway/ipc-server.ts +29 -0
- package/telegram-plugin/gateway/linear-activity.ts +145 -0
- package/telegram-plugin/runtime-metrics.ts +14 -0
- package/telegram-plugin/scoped-approval.ts +253 -0
- package/telegram-plugin/tests/bridge-liveness-override.test.ts +21 -0
- package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +54 -0
- package/telegram-plugin/tests/linear-agent-activity.test.ts +1 -1
- package/telegram-plugin/tests/linear-create-issue.test.ts +211 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +13 -0
- package/telegram-plugin/tests/runtime-metrics.test.ts +9 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +254 -0
- package/telegram-plugin/tests/send-outbound-wiring.test.ts +63 -0
|
@@ -23802,7 +23802,7 @@ var init_dist = __esm(() => {
|
|
|
23802
23802
|
});
|
|
23803
23803
|
|
|
23804
23804
|
// ../src/config/schema.ts
|
|
23805
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
23805
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
|
|
23806
23806
|
var init_schema = __esm(() => {
|
|
23807
23807
|
init_zod();
|
|
23808
23808
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -23835,11 +23835,29 @@ var init_schema = __esm(() => {
|
|
|
23835
23835
|
HttpDiffPollSchema,
|
|
23836
23836
|
TelegramReactionsPollSchema
|
|
23837
23837
|
]);
|
|
23838
|
+
TelegramMessageActionSchema = exports_external.object({
|
|
23839
|
+
type: exports_external.literal("telegram-message"),
|
|
23840
|
+
text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable \u2014 fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output \u2014 the text is fully determined by config."),
|
|
23841
|
+
parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
|
|
23842
|
+
});
|
|
23843
|
+
WebhookActionSchema = exports_external.object({
|
|
23844
|
+
type: exports_external.literal("webhook"),
|
|
23845
|
+
url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (\u00a76.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
|
|
23846
|
+
method: exports_external.enum(["GET", "POST"]).default("POST"),
|
|
23847
|
+
headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
|
|
23848
|
+
body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
|
|
23849
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (\u00a76.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
|
|
23850
|
+
});
|
|
23851
|
+
ActionSpecSchema = exports_external.discriminatedUnion("type", [
|
|
23852
|
+
TelegramMessageActionSchema,
|
|
23853
|
+
WebhookActionSchema
|
|
23854
|
+
]);
|
|
23838
23855
|
ScheduleEntrySchema = exports_external.object({
|
|
23839
23856
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
23840
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
23841
|
-
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit.
|
|
23857
|
+
prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
|
|
23858
|
+
kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
|
|
23842
23859
|
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
23860
|
+
action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
|
|
23843
23861
|
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
23844
23862
|
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' \u2192 a minimal-" + "context cheap cron session (Tier 1). 'agent' \u2192 the agent's live " + "session with full persona/memory (Tier 2). Unset \u2192 inferred from " + "`model` (cheap\u2192fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
23845
23863
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default \u2014 broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary \u2014 " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
@@ -23856,13 +23874,41 @@ var init_schema = __esm(() => {
|
|
|
23856
23874
|
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
23857
23875
|
});
|
|
23858
23876
|
}
|
|
23859
|
-
if (kind
|
|
23877
|
+
if (kind !== "poll" && entry.poll) {
|
|
23860
23878
|
ctx.addIssue({
|
|
23861
23879
|
code: exports_external.ZodIssueCode.custom,
|
|
23862
23880
|
path: ["poll"],
|
|
23863
23881
|
message: "`poll` is only valid when kind: poll."
|
|
23864
23882
|
});
|
|
23865
23883
|
}
|
|
23884
|
+
if (kind === "action" && !entry.action) {
|
|
23885
|
+
ctx.addIssue({
|
|
23886
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23887
|
+
path: ["action"],
|
|
23888
|
+
message: "kind: action requires an `action` spec (telegram-message or webhook)."
|
|
23889
|
+
});
|
|
23890
|
+
}
|
|
23891
|
+
if (kind !== "action" && entry.action) {
|
|
23892
|
+
ctx.addIssue({
|
|
23893
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23894
|
+
path: ["action"],
|
|
23895
|
+
message: "`action` is only valid when kind: action."
|
|
23896
|
+
});
|
|
23897
|
+
}
|
|
23898
|
+
if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
|
|
23899
|
+
ctx.addIssue({
|
|
23900
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23901
|
+
path: ["prompt"],
|
|
23902
|
+
message: `kind: ${kind} requires a non-empty \`prompt\`.`
|
|
23903
|
+
});
|
|
23904
|
+
}
|
|
23905
|
+
if (kind === "action" && entry.prompt !== undefined) {
|
|
23906
|
+
ctx.addIssue({
|
|
23907
|
+
code: exports_external.ZodIssueCode.custom,
|
|
23908
|
+
path: ["prompt"],
|
|
23909
|
+
message: "`prompt` is not valid for kind: action (an action never fires a model)."
|
|
23910
|
+
});
|
|
23911
|
+
}
|
|
23866
23912
|
});
|
|
23867
23913
|
AgentSoulSchema = exports_external.object({
|
|
23868
23914
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -23996,7 +24042,8 @@ var init_schema = __esm(() => {
|
|
|
23996
24042
|
linear_agent: exports_external.object({
|
|
23997
24043
|
enabled: exports_external.boolean(),
|
|
23998
24044
|
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.")
|
|
24045
|
+
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."),
|
|
24046
|
+
default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
|
|
24000
24047
|
}).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
|
|
24001
24048
|
chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
|
|
24002
24049
|
default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
|
|
@@ -24880,6 +24927,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
24880
24927
|
}
|
|
24881
24928
|
merged.reaction_dispatch = combined;
|
|
24882
24929
|
}
|
|
24930
|
+
const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
|
|
24931
|
+
if (linearEnabled) {
|
|
24932
|
+
const rd = merged.reaction_dispatch;
|
|
24933
|
+
if (!rd || rd.emojis === undefined) {
|
|
24934
|
+
merged.reaction_dispatch = {
|
|
24935
|
+
enabled: rd?.enabled ?? true,
|
|
24936
|
+
emojis: ["\uD83D\uDC68\u200d\uD83D\uDCBB", "\uD83D\uDCCC"]
|
|
24937
|
+
};
|
|
24938
|
+
}
|
|
24939
|
+
}
|
|
24883
24940
|
if (defaults.resources || merged.resources) {
|
|
24884
24941
|
const d = defaults.resources ?? {};
|
|
24885
24942
|
const a = merged.resources ?? {};
|
|
@@ -31455,7 +31512,7 @@ import {
|
|
|
31455
31512
|
appendFileSync as appendFileSync5
|
|
31456
31513
|
} from "fs";
|
|
31457
31514
|
import { homedir as homedir14 } from "os";
|
|
31458
|
-
import { join as join35, extname, sep as sep3, basename as
|
|
31515
|
+
import { join as join35, extname, sep as sep3, basename as basename9 } from "path";
|
|
31459
31516
|
|
|
31460
31517
|
// plugin-logger.ts
|
|
31461
31518
|
import { appendFileSync, mkdirSync, renameSync, statSync, existsSync } from "fs";
|
|
@@ -45826,6 +45883,16 @@ function mergeAgentConfig2(defaultsIn, agentIn) {
|
|
|
45826
45883
|
}
|
|
45827
45884
|
merged.reaction_dispatch = combined;
|
|
45828
45885
|
}
|
|
45886
|
+
const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
|
|
45887
|
+
if (linearEnabled) {
|
|
45888
|
+
const rd = merged.reaction_dispatch;
|
|
45889
|
+
if (!rd || rd.emojis === undefined) {
|
|
45890
|
+
merged.reaction_dispatch = {
|
|
45891
|
+
enabled: rd?.enabled ?? true,
|
|
45892
|
+
emojis: ["\uD83D\uDC68\u200d\uD83D\uDCBB", "\uD83D\uDCCC"]
|
|
45893
|
+
};
|
|
45894
|
+
}
|
|
45895
|
+
}
|
|
45829
45896
|
if (defaults.resources || merged.resources) {
|
|
45830
45897
|
const d = defaults.resources ?? {};
|
|
45831
45898
|
const a = merged.resources ?? {};
|
|
@@ -46731,6 +46798,17 @@ function createInjectIpcClient(options) {
|
|
|
46731
46798
|
return false;
|
|
46732
46799
|
try {
|
|
46733
46800
|
return socket.write(JSON.stringify(msg) + `
|
|
46801
|
+
`);
|
|
46802
|
+
} catch (err) {
|
|
46803
|
+
log(`scheduler ipc: write failed: ${err.message}`);
|
|
46804
|
+
return false;
|
|
46805
|
+
}
|
|
46806
|
+
},
|
|
46807
|
+
sendOutbound(msg) {
|
|
46808
|
+
if (!socket || !connected)
|
|
46809
|
+
return false;
|
|
46810
|
+
try {
|
|
46811
|
+
return socket.write(JSON.stringify(msg) + `
|
|
46734
46812
|
`);
|
|
46735
46813
|
} catch (err) {
|
|
46736
46814
|
log(`scheduler ipc: write failed: ${err.message}`);
|
|
@@ -47306,6 +47384,19 @@ function validateClientMessage(msg) {
|
|
|
47306
47384
|
const inb = m.inbound;
|
|
47307
47385
|
return inb.type === "inbound" && typeof inb.chatId === "string" && inb.chatId.length > 0 && typeof inb.text === "string" && typeof inb.messageId === "number" && typeof inb.user === "string" && typeof inb.userId === "number" && typeof inb.ts === "number" && typeof inb.meta === "object" && inb.meta !== null;
|
|
47308
47386
|
}
|
|
47387
|
+
case "send_outbound": {
|
|
47388
|
+
if (typeof m.agentName !== "string" || !AGENT_NAME_RE3.test(m.agentName))
|
|
47389
|
+
return false;
|
|
47390
|
+
if (typeof m.chatId !== "string" || m.chatId.length === 0)
|
|
47391
|
+
return false;
|
|
47392
|
+
if (typeof m.text !== "string" || m.text.length === 0 || m.text.length > 4096)
|
|
47393
|
+
return false;
|
|
47394
|
+
if (m.threadId !== undefined && (typeof m.threadId !== "number" || !Number.isInteger(m.threadId)))
|
|
47395
|
+
return false;
|
|
47396
|
+
if (m.parseMode !== undefined && m.parseMode !== "html" && m.parseMode !== "text")
|
|
47397
|
+
return false;
|
|
47398
|
+
return true;
|
|
47399
|
+
}
|
|
47309
47400
|
case "quota_wall_detected": {
|
|
47310
47401
|
if (typeof m.agentName !== "string" || !AGENT_NAME_RE3.test(m.agentName))
|
|
47311
47402
|
return false;
|
|
@@ -47374,6 +47465,7 @@ function createIpcServer(options) {
|
|
|
47374
47465
|
onOperatorEvent,
|
|
47375
47466
|
onPtyPartial,
|
|
47376
47467
|
onInjectInbound,
|
|
47468
|
+
onSendOutbound,
|
|
47377
47469
|
onQuotaWallDetected,
|
|
47378
47470
|
onRequestDriveApproval,
|
|
47379
47471
|
onRequestMs365Approval,
|
|
@@ -47459,6 +47551,10 @@ function createIpcServer(options) {
|
|
|
47459
47551
|
if (onInjectInbound)
|
|
47460
47552
|
onInjectInbound(client3, msg);
|
|
47461
47553
|
break;
|
|
47554
|
+
case "send_outbound":
|
|
47555
|
+
if (onSendOutbound)
|
|
47556
|
+
onSendOutbound(client3, msg);
|
|
47557
|
+
break;
|
|
47462
47558
|
case "quota_wall_detected":
|
|
47463
47559
|
if (onQuotaWallDetected)
|
|
47464
47560
|
onQuotaWallDetected(client3, msg);
|
|
@@ -52781,9 +52877,10 @@ function defaultReadEvents(stateDir) {
|
|
|
52781
52877
|
return readAll(stateDir);
|
|
52782
52878
|
}
|
|
52783
52879
|
// permission-title.ts
|
|
52784
|
-
import { basename as
|
|
52880
|
+
import { basename as basename6 } from "node:path";
|
|
52785
52881
|
|
|
52786
52882
|
// permission-rule.ts
|
|
52883
|
+
import { basename as basename5 } from "node:path";
|
|
52787
52884
|
var FILE_TOOLS = new Set([
|
|
52788
52885
|
"Edit",
|
|
52789
52886
|
"Write",
|
|
@@ -52804,6 +52901,83 @@ var BROAD_ONLY_TOOLS = new Set([
|
|
|
52804
52901
|
function prettyMcpServer(server) {
|
|
52805
52902
|
return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
52806
52903
|
}
|
|
52904
|
+
function resolveSkillName(input) {
|
|
52905
|
+
return readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
|
|
52906
|
+
}
|
|
52907
|
+
function filePathFrom(input) {
|
|
52908
|
+
if (!input)
|
|
52909
|
+
return null;
|
|
52910
|
+
return readString(input, "file_path") ?? readString(input, "notebook_path");
|
|
52911
|
+
}
|
|
52912
|
+
function bashFirstToken(command) {
|
|
52913
|
+
const m = /^\s*([^\s|&;<>()`$]+)/.exec(command);
|
|
52914
|
+
if (!m)
|
|
52915
|
+
return null;
|
|
52916
|
+
const tok = m[1];
|
|
52917
|
+
if (tok.includes(".."))
|
|
52918
|
+
return null;
|
|
52919
|
+
return /^[A-Za-z0-9._\-\/]+$/.test(tok) ? tok : null;
|
|
52920
|
+
}
|
|
52921
|
+
function parseInput(raw) {
|
|
52922
|
+
if (!raw || typeof raw !== "string")
|
|
52923
|
+
return null;
|
|
52924
|
+
const trimmed = raw.trim();
|
|
52925
|
+
if (!trimmed.startsWith("{"))
|
|
52926
|
+
return null;
|
|
52927
|
+
try {
|
|
52928
|
+
const parsed = JSON.parse(trimmed);
|
|
52929
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
52930
|
+
return parsed;
|
|
52931
|
+
}
|
|
52932
|
+
} catch {}
|
|
52933
|
+
return null;
|
|
52934
|
+
}
|
|
52935
|
+
function readString(input, key) {
|
|
52936
|
+
const value = input[key];
|
|
52937
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
52938
|
+
}
|
|
52939
|
+
function skillBasenameFromPath(input) {
|
|
52940
|
+
const path = readString(input, "path") ?? readString(input, "skill_path");
|
|
52941
|
+
if (!path)
|
|
52942
|
+
return null;
|
|
52943
|
+
const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
52944
|
+
return basename5(trimmed) || null;
|
|
52945
|
+
}
|
|
52946
|
+
function matchesAllowRule(rule, toolName, inputPreview) {
|
|
52947
|
+
if (!rule || !toolName)
|
|
52948
|
+
return false;
|
|
52949
|
+
if (rule.endsWith("__*") && rule.startsWith("mcp__")) {
|
|
52950
|
+
const prefix = rule.slice(0, -1);
|
|
52951
|
+
return toolName.startsWith(prefix);
|
|
52952
|
+
}
|
|
52953
|
+
const scoped = /^([A-Za-z]+)\((.+)\)$/.exec(rule);
|
|
52954
|
+
if (scoped) {
|
|
52955
|
+
const ruleTool = scoped[1];
|
|
52956
|
+
const arg = scoped[2];
|
|
52957
|
+
if (ruleTool !== toolName)
|
|
52958
|
+
return false;
|
|
52959
|
+
const input = parseInput(inputPreview);
|
|
52960
|
+
if (ruleTool === "Skill") {
|
|
52961
|
+
if (!input)
|
|
52962
|
+
return false;
|
|
52963
|
+
return resolveSkillName(input) === arg;
|
|
52964
|
+
}
|
|
52965
|
+
if (ruleTool === "Bash") {
|
|
52966
|
+
const cmd = input ? readString(input, "command") : null;
|
|
52967
|
+
if (!cmd)
|
|
52968
|
+
return false;
|
|
52969
|
+
const m = /^([^:]+):\*$/.exec(arg);
|
|
52970
|
+
if (!m)
|
|
52971
|
+
return false;
|
|
52972
|
+
return bashFirstToken(cmd) === m[1];
|
|
52973
|
+
}
|
|
52974
|
+
if (FILE_TOOLS.has(ruleTool)) {
|
|
52975
|
+
return filePathFrom(input) === arg;
|
|
52976
|
+
}
|
|
52977
|
+
return false;
|
|
52978
|
+
}
|
|
52979
|
+
return rule === toolName;
|
|
52980
|
+
}
|
|
52807
52981
|
|
|
52808
52982
|
// permission-title.ts
|
|
52809
52983
|
init_redact();
|
|
@@ -52862,7 +53036,7 @@ function formatPermissionCardBody(opts) {
|
|
|
52862
53036
|
`);
|
|
52863
53037
|
}
|
|
52864
53038
|
function naturalAction(toolName, inputPreview) {
|
|
52865
|
-
const input =
|
|
53039
|
+
const input = parseInput2(inputPreview);
|
|
52866
53040
|
if (toolName.startsWith("mcp__"))
|
|
52867
53041
|
return naturalMcpAction(toolName, input);
|
|
52868
53042
|
switch (toolName) {
|
|
@@ -52881,24 +53055,24 @@ function naturalAction(toolName, inputPreview) {
|
|
|
52881
53055
|
return f ? `read: ${f}` : "read files";
|
|
52882
53056
|
}
|
|
52883
53057
|
case "Bash": {
|
|
52884
|
-
const c = input ?
|
|
53058
|
+
const c = input ? readString2(input, "command") : null;
|
|
52885
53059
|
return c ? `run: ${truncate6(c, COMMAND_TITLE_MAX)}` : "run shell commands";
|
|
52886
53060
|
}
|
|
52887
53061
|
case "Skill": {
|
|
52888
|
-
const s = input ?
|
|
53062
|
+
const s = input ? resolveSkillName2(input) : null;
|
|
52889
53063
|
return s ? `use the ${s} skill` : "use a skill";
|
|
52890
53064
|
}
|
|
52891
53065
|
case "Glob":
|
|
52892
53066
|
case "Grep": {
|
|
52893
|
-
const p = input ?
|
|
53067
|
+
const p = input ? readString2(input, "pattern") : null;
|
|
52894
53068
|
return p ? `search files for: ${truncate6(p, COMMAND_TITLE_MAX)}` : "search files";
|
|
52895
53069
|
}
|
|
52896
53070
|
case "WebSearch": {
|
|
52897
|
-
const q = input ?
|
|
53071
|
+
const q = input ? readString2(input, "query") : null;
|
|
52898
53072
|
return q ? `search the web for: ${truncate6(q, COMMAND_TITLE_MAX)}` : "search the web";
|
|
52899
53073
|
}
|
|
52900
53074
|
case "WebFetch": {
|
|
52901
|
-
const u = input ?
|
|
53075
|
+
const u = input ? readString2(input, "url") : null;
|
|
52902
53076
|
return u ? `fetch a web page: ${truncate6(u, COMMAND_TITLE_MAX)}` : "fetch a web page";
|
|
52903
53077
|
}
|
|
52904
53078
|
case "Task":
|
|
@@ -52936,7 +53110,7 @@ function restResourcePhrase(server, verb, input) {
|
|
|
52936
53110
|
return null;
|
|
52937
53111
|
let path = null;
|
|
52938
53112
|
for (const key of RESOURCE_KEYS) {
|
|
52939
|
-
path =
|
|
53113
|
+
path = readString2(input, key);
|
|
52940
53114
|
if (path)
|
|
52941
53115
|
break;
|
|
52942
53116
|
}
|
|
@@ -52952,7 +53126,7 @@ function mcpArgSummary(toolName, inputPreview) {
|
|
|
52952
53126
|
const server = toolName.split("__")[1] ?? "";
|
|
52953
53127
|
if (INTERNAL_MCP_SERVERS.has(server))
|
|
52954
53128
|
return null;
|
|
52955
|
-
const input =
|
|
53129
|
+
const input = parseInput2(inputPreview);
|
|
52956
53130
|
if (!input)
|
|
52957
53131
|
return null;
|
|
52958
53132
|
const payload = input.body ?? input.query;
|
|
@@ -52996,11 +53170,11 @@ function describeGrant(toolName, inputPreview, option) {
|
|
|
52996
53170
|
return m ? `run ${m[1]} commands` : "run that command";
|
|
52997
53171
|
}
|
|
52998
53172
|
if (t === "Edit" || t === "MultiEdit" || t === "NotebookEdit")
|
|
52999
|
-
return `edit ${
|
|
53173
|
+
return `edit ${basename6(arg)}`;
|
|
53000
53174
|
if (t === "Write")
|
|
53001
|
-
return `write ${
|
|
53175
|
+
return `write ${basename6(arg)}`;
|
|
53002
53176
|
if (t === "Read")
|
|
53003
|
-
return `read ${
|
|
53177
|
+
return `read ${basename6(arg)}`;
|
|
53004
53178
|
return naturalAction(toolName, inputPreview);
|
|
53005
53179
|
}
|
|
53006
53180
|
switch (rule) {
|
|
@@ -53032,14 +53206,14 @@ function formatPermissionResumeMessage(opts) {
|
|
|
53032
53206
|
}
|
|
53033
53207
|
return hasAction ? `\uD83D\uDEAB ${who} \u2014 noted, I won't ${escapeTgHtml(lowerFirst(act))}. Continuing without it.` : `\uD83D\uDEAB ${who} \u2014 noted, continuing without it.`;
|
|
53034
53208
|
}
|
|
53035
|
-
function
|
|
53036
|
-
return
|
|
53209
|
+
function resolveSkillName2(input) {
|
|
53210
|
+
return readString2(input, "skill") ?? readString2(input, "skill_name") ?? readString2(input, "skillName") ?? readString2(input, "name") ?? skillBasenameFromPath2(input);
|
|
53037
53211
|
}
|
|
53038
53212
|
function fileBase(input) {
|
|
53039
53213
|
if (!input)
|
|
53040
53214
|
return null;
|
|
53041
|
-
const p =
|
|
53042
|
-
return p ?
|
|
53215
|
+
const p = readString2(input, "file_path") ?? readString2(input, "notebook_path");
|
|
53216
|
+
return p ? basename6(p) : null;
|
|
53043
53217
|
}
|
|
53044
53218
|
function lowerFirst(text) {
|
|
53045
53219
|
return text.length > 0 ? text.charAt(0).toLowerCase() + text.slice(1) : text;
|
|
@@ -53050,7 +53224,7 @@ function capFirst(text) {
|
|
|
53050
53224
|
function escapeTgHtml(text) {
|
|
53051
53225
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
53052
53226
|
}
|
|
53053
|
-
function
|
|
53227
|
+
function parseInput2(raw) {
|
|
53054
53228
|
if (!raw || typeof raw !== "string")
|
|
53055
53229
|
return null;
|
|
53056
53230
|
const trimmed = raw.trim();
|
|
@@ -53064,12 +53238,12 @@ function parseInput(raw) {
|
|
|
53064
53238
|
} catch {}
|
|
53065
53239
|
return null;
|
|
53066
53240
|
}
|
|
53067
|
-
function
|
|
53241
|
+
function readString2(input, key) {
|
|
53068
53242
|
const value = input[key];
|
|
53069
53243
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
53070
53244
|
}
|
|
53071
|
-
function
|
|
53072
|
-
const path =
|
|
53245
|
+
function skillBasenameFromPath2(input) {
|
|
53246
|
+
const path = readString2(input, "path") ?? readString2(input, "skill_path");
|
|
53073
53247
|
if (!path)
|
|
53074
53248
|
return null;
|
|
53075
53249
|
const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
@@ -53085,7 +53259,7 @@ function truncate6(text, max) {
|
|
|
53085
53259
|
}
|
|
53086
53260
|
|
|
53087
53261
|
// permission-rule.ts
|
|
53088
|
-
import { basename as
|
|
53262
|
+
import { basename as basename7 } from "node:path";
|
|
53089
53263
|
var FILE_TOOLS2 = new Set([
|
|
53090
53264
|
"Edit",
|
|
53091
53265
|
"Write",
|
|
@@ -53106,9 +53280,9 @@ var BROAD_ONLY_TOOLS2 = new Set([
|
|
|
53106
53280
|
function resolveScopedAllowChoices(toolName, inputPreview) {
|
|
53107
53281
|
if (!toolName)
|
|
53108
53282
|
return null;
|
|
53109
|
-
const input =
|
|
53283
|
+
const input = parseInput3(inputPreview);
|
|
53110
53284
|
if (toolName === "Skill") {
|
|
53111
|
-
const skill = input ?
|
|
53285
|
+
const skill = input ? resolveSkillName3(input) : null;
|
|
53112
53286
|
if (!skill)
|
|
53113
53287
|
return null;
|
|
53114
53288
|
if (!/^[A-Za-z0-9._\-+]+$/.test(skill))
|
|
@@ -53119,7 +53293,7 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
|
|
|
53119
53293
|
};
|
|
53120
53294
|
}
|
|
53121
53295
|
if (FILE_TOOLS2.has(toolName)) {
|
|
53122
|
-
const path =
|
|
53296
|
+
const path = filePathFrom2(input);
|
|
53123
53297
|
const broad = { rule: toolName, buttonLabel: "Any file", broad: true };
|
|
53124
53298
|
if (path) {
|
|
53125
53299
|
return {
|
|
@@ -53131,8 +53305,8 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
|
|
|
53131
53305
|
}
|
|
53132
53306
|
if (toolName === "Bash") {
|
|
53133
53307
|
const broad = { rule: "Bash", buttonLabel: "Any command", broad: true };
|
|
53134
|
-
const cmd = input ?
|
|
53135
|
-
const tok = cmd ?
|
|
53308
|
+
const cmd = input ? readString3(input, "command") : null;
|
|
53309
|
+
const tok = cmd ? bashFirstToken2(cmd) : null;
|
|
53136
53310
|
if (tok) {
|
|
53137
53311
|
return {
|
|
53138
53312
|
specific: { rule: `Bash(${tok}:*)`, buttonLabel: `${tok} commands`, broad: false },
|
|
@@ -53160,15 +53334,15 @@ function resolveScopedAllowChoices(toolName, inputPreview) {
|
|
|
53160
53334
|
function prettyMcpServer2(server) {
|
|
53161
53335
|
return server.split(/[-_]/).filter((w) => w.length > 0).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
53162
53336
|
}
|
|
53163
|
-
function
|
|
53164
|
-
return
|
|
53337
|
+
function resolveSkillName3(input) {
|
|
53338
|
+
return readString3(input, "skill") ?? readString3(input, "skill_name") ?? readString3(input, "skillName") ?? readString3(input, "name") ?? skillBasenameFromPath3(input);
|
|
53165
53339
|
}
|
|
53166
|
-
function
|
|
53340
|
+
function filePathFrom2(input) {
|
|
53167
53341
|
if (!input)
|
|
53168
53342
|
return null;
|
|
53169
|
-
return
|
|
53343
|
+
return readString3(input, "file_path") ?? readString3(input, "notebook_path");
|
|
53170
53344
|
}
|
|
53171
|
-
function
|
|
53345
|
+
function bashFirstToken2(command) {
|
|
53172
53346
|
const m = /^\s*([^\s|&;<>()`$]+)/.exec(command);
|
|
53173
53347
|
if (!m)
|
|
53174
53348
|
return null;
|
|
@@ -53177,7 +53351,7 @@ function bashFirstToken(command) {
|
|
|
53177
53351
|
return null;
|
|
53178
53352
|
return /^[A-Za-z0-9._\-\/]+$/.test(tok) ? tok : null;
|
|
53179
53353
|
}
|
|
53180
|
-
function
|
|
53354
|
+
function parseInput3(raw) {
|
|
53181
53355
|
if (!raw || typeof raw !== "string")
|
|
53182
53356
|
return null;
|
|
53183
53357
|
const trimmed = raw.trim();
|
|
@@ -53191,21 +53365,136 @@ function parseInput2(raw) {
|
|
|
53191
53365
|
} catch {}
|
|
53192
53366
|
return null;
|
|
53193
53367
|
}
|
|
53194
|
-
function
|
|
53368
|
+
function readString3(input, key) {
|
|
53195
53369
|
const value = input[key];
|
|
53196
53370
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
53197
53371
|
}
|
|
53198
|
-
function
|
|
53199
|
-
const path =
|
|
53372
|
+
function skillBasenameFromPath3(input) {
|
|
53373
|
+
const path = readString3(input, "path") ?? readString3(input, "skill_path");
|
|
53200
53374
|
if (!path)
|
|
53201
53375
|
return null;
|
|
53202
53376
|
const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
53203
|
-
return
|
|
53377
|
+
return basename7(trimmed) || null;
|
|
53204
53378
|
}
|
|
53205
53379
|
function isRulePersisted(resolvedAllow, ruleRule) {
|
|
53206
53380
|
return resolvedAllow.includes(ruleRule);
|
|
53207
53381
|
}
|
|
53208
53382
|
|
|
53383
|
+
// scoped-approval.ts
|
|
53384
|
+
import { basename as basename8 } from "node:path";
|
|
53385
|
+
var SCOPED_APPROVAL_DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
53386
|
+
function scopedApprovalTtlMs(env = process.env) {
|
|
53387
|
+
const raw = env.SWITCHROOM_SCOPED_APPROVAL_TTL_MS;
|
|
53388
|
+
if (raw === undefined || raw.trim() === "")
|
|
53389
|
+
return SCOPED_APPROVAL_DEFAULT_TTL_MS;
|
|
53390
|
+
const n = Number(raw);
|
|
53391
|
+
if (!Number.isFinite(n) || n < 0)
|
|
53392
|
+
return SCOPED_APPROVAL_DEFAULT_TTL_MS;
|
|
53393
|
+
return Math.floor(n);
|
|
53394
|
+
}
|
|
53395
|
+
var FILE_RULE = /^(Edit|Write|MultiEdit|NotebookEdit|Read)\((.+)\)$/;
|
|
53396
|
+
var BASH_FAMILY_RULE = /^Bash\(([^:]+):\*\)$/;
|
|
53397
|
+
function resolveTimeBox(toolName, inputPreview, choices) {
|
|
53398
|
+
const specific = choices?.specific;
|
|
53399
|
+
if (!specific || specific.broad)
|
|
53400
|
+
return null;
|
|
53401
|
+
const rule = specific.rule;
|
|
53402
|
+
const fileMatch = FILE_RULE.exec(rule);
|
|
53403
|
+
if (fileMatch) {
|
|
53404
|
+
const verb = fileMatch[1] === "Read" ? "reads of" : "edits to";
|
|
53405
|
+
return { rule, breadth: `${verb} ${basename8(fileMatch[2])}` };
|
|
53406
|
+
}
|
|
53407
|
+
const bashMatch = BASH_FAMILY_RULE.exec(rule);
|
|
53408
|
+
if (bashMatch) {
|
|
53409
|
+
const cmd = readBashCommand(inputPreview);
|
|
53410
|
+
if (!cmd || isDestructiveBashCommand(cmd))
|
|
53411
|
+
return null;
|
|
53412
|
+
return { rule, breadth: `any \`${bashMatch[1]}\` command` };
|
|
53413
|
+
}
|
|
53414
|
+
return null;
|
|
53415
|
+
}
|
|
53416
|
+
function recordScopedGrant(store2, agent, rule, now, ttlMs) {
|
|
53417
|
+
if (ttlMs <= 0)
|
|
53418
|
+
return;
|
|
53419
|
+
const list2 = store2.get(agent) ?? [];
|
|
53420
|
+
const others = list2.filter((g) => g.rule !== rule);
|
|
53421
|
+
others.push({ rule, expiresAt: now + ttlMs });
|
|
53422
|
+
store2.set(agent, others);
|
|
53423
|
+
}
|
|
53424
|
+
function lookupScopedGrant(store2, agent, toolName, inputPreview, now) {
|
|
53425
|
+
const list2 = store2.get(agent);
|
|
53426
|
+
if (!list2 || list2.length === 0)
|
|
53427
|
+
return null;
|
|
53428
|
+
for (const g of list2) {
|
|
53429
|
+
if (g.expiresAt <= now)
|
|
53430
|
+
continue;
|
|
53431
|
+
if (!matchesAllowRule(g.rule, toolName, inputPreview))
|
|
53432
|
+
continue;
|
|
53433
|
+
if (toolName === "Bash") {
|
|
53434
|
+
const cmd = readBashCommand(inputPreview);
|
|
53435
|
+
if (!cmd || isDestructiveBashCommand(cmd))
|
|
53436
|
+
return null;
|
|
53437
|
+
}
|
|
53438
|
+
return g.rule;
|
|
53439
|
+
}
|
|
53440
|
+
return null;
|
|
53441
|
+
}
|
|
53442
|
+
function sweepScopedGrants(store2, now) {
|
|
53443
|
+
for (const [agent, list2] of store2) {
|
|
53444
|
+
const live = list2.filter((g) => g.expiresAt > now);
|
|
53445
|
+
if (live.length === 0)
|
|
53446
|
+
store2.delete(agent);
|
|
53447
|
+
else if (live.length !== list2.length)
|
|
53448
|
+
store2.set(agent, live);
|
|
53449
|
+
}
|
|
53450
|
+
}
|
|
53451
|
+
function isDestructiveBashCommand(command) {
|
|
53452
|
+
if (!command || !command.trim())
|
|
53453
|
+
return true;
|
|
53454
|
+
const c = command.toLowerCase();
|
|
53455
|
+
if (c.includes("`"))
|
|
53456
|
+
return true;
|
|
53457
|
+
if (/\|\s*(sudo\s+)?(sh|bash|zsh|fish|python\d?|perl|ruby|node)\b/.test(c))
|
|
53458
|
+
return true;
|
|
53459
|
+
if (/(^|\s|;|&&|\|\||\()sudo\b/.test(c))
|
|
53460
|
+
return true;
|
|
53461
|
+
if (/(^|\s|;|&&|\|\||\()(su|doas)\s/.test(c))
|
|
53462
|
+
return true;
|
|
53463
|
+
if (/(^|\s|;|&&|\|\||\()(rm|rmdir|dd|shred|truncate|fdisk|mkfs\S*|wipefs|blkdiscard|fallocate)\b/.test(c))
|
|
53464
|
+
return true;
|
|
53465
|
+
if (/\b(chmod|chown|chgrp)\b[^|;&]*(\s-(-recursive|[a-z]*r[a-z]*)\b)/.test(c))
|
|
53466
|
+
return true;
|
|
53467
|
+
if (/>\s*\/(dev|etc|boot|sys|proc)\b/.test(c))
|
|
53468
|
+
return true;
|
|
53469
|
+
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))
|
|
53470
|
+
return true;
|
|
53471
|
+
if (/(^|\s|;|&&|\|\||\()(shutdown|reboot|halt|poweroff|kill|killall|pkill)\b/.test(c))
|
|
53472
|
+
return true;
|
|
53473
|
+
if (/(^|\s)init\s+0\b/.test(c))
|
|
53474
|
+
return true;
|
|
53475
|
+
if (/\bdocker\b[^|;&]*\b(rm|prune)\b/.test(c))
|
|
53476
|
+
return true;
|
|
53477
|
+
if (/(^|\s)(apt|apt-get|yum|dnf|brew|pacman|npm|pnpm|yarn|pip\d?)\b[^|;&]*\b(remove|uninstall|purge|prune)\b/.test(c))
|
|
53478
|
+
return true;
|
|
53479
|
+
if (/:\s*\(\s*\)\s*\{/.test(c))
|
|
53480
|
+
return true;
|
|
53481
|
+
return false;
|
|
53482
|
+
}
|
|
53483
|
+
function readBashCommand(inputPreview) {
|
|
53484
|
+
if (!inputPreview || typeof inputPreview !== "string")
|
|
53485
|
+
return null;
|
|
53486
|
+
const trimmed = inputPreview.trim();
|
|
53487
|
+
if (!trimmed.startsWith("{"))
|
|
53488
|
+
return null;
|
|
53489
|
+
try {
|
|
53490
|
+
const parsed = JSON.parse(trimmed);
|
|
53491
|
+
const cmd = parsed?.command;
|
|
53492
|
+
return typeof cmd === "string" && cmd.length > 0 ? cmd : null;
|
|
53493
|
+
} catch {
|
|
53494
|
+
return null;
|
|
53495
|
+
}
|
|
53496
|
+
}
|
|
53497
|
+
|
|
53209
53498
|
// permission-diff.ts
|
|
53210
53499
|
var TARGET_HEADER_A = "--- a/switchroom.yaml";
|
|
53211
53500
|
var TARGET_HEADER_B = "+++ b/switchroom.yaml";
|
|
@@ -53877,10 +54166,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53877
54166
|
}
|
|
53878
54167
|
|
|
53879
54168
|
// ../src/build-info.ts
|
|
53880
|
-
var VERSION = "0.15.
|
|
53881
|
-
var COMMIT_SHA = "
|
|
53882
|
-
var COMMIT_DATE = "2026-06-
|
|
53883
|
-
var LATEST_PR =
|
|
54169
|
+
var VERSION = "0.15.14";
|
|
54170
|
+
var COMMIT_SHA = "91ae15d3";
|
|
54171
|
+
var COMMIT_DATE = "2026-06-13T11:51:08Z";
|
|
54172
|
+
var LATEST_PR = 2326;
|
|
53884
54173
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53885
54174
|
|
|
53886
54175
|
// gateway/boot-version.ts
|
|
@@ -54274,6 +54563,106 @@ async function emitLinearAgentActivity(args, deps = {}) {
|
|
|
54274
54563
|
`);
|
|
54275
54564
|
return { content: [{ type: "text", text: `Linear ${type} emitted on session ${sessionId}` }] };
|
|
54276
54565
|
}
|
|
54566
|
+
function captureDedupMarker(dedupKey) {
|
|
54567
|
+
return `
|
|
54568
|
+
|
|
54569
|
+
<!-- switchroom-capture: ${dedupKey} -->`;
|
|
54570
|
+
}
|
|
54571
|
+
async function createLinearIssue(args, deps = {}) {
|
|
54572
|
+
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
54573
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
54574
|
+
const title = args.title;
|
|
54575
|
+
if (!title || title.trim() === "")
|
|
54576
|
+
throw new Error("linear_create_issue: title is required");
|
|
54577
|
+
const body = args.body ?? "";
|
|
54578
|
+
const teamIdArg = args.team_id ?? (deps.defaultTeamId ?? process.env.SWITCHROOM_LINEAR_DEFAULT_TEAM_ID) ?? undefined;
|
|
54579
|
+
const dedupKey = args.dedup_key ?? undefined;
|
|
54580
|
+
const priority = typeof args.priority === "number" ? args.priority : undefined;
|
|
54581
|
+
const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? "-";
|
|
54582
|
+
const resolveToken = deps.resolveToken ?? defaultResolveLinearToken;
|
|
54583
|
+
const tokenResult = await resolveToken(agent);
|
|
54584
|
+
if (!tokenResult.ok) {
|
|
54585
|
+
const hint = tokenResult.reason === "denied" || tokenResult.reason === "not_found" ? ` Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.` : "";
|
|
54586
|
+
return {
|
|
54587
|
+
content: [
|
|
54588
|
+
{ type: "text", text: `Couldn't file to Linear: no token (vault ${tokenResult.reason}).${hint}` }
|
|
54589
|
+
]
|
|
54590
|
+
};
|
|
54591
|
+
}
|
|
54592
|
+
const token = tokenResult.token;
|
|
54593
|
+
const gql = async (query2, variables) => {
|
|
54594
|
+
let resp;
|
|
54595
|
+
try {
|
|
54596
|
+
resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
|
|
54597
|
+
method: "POST",
|
|
54598
|
+
headers: { "Content-Type": "application/json", Authorization: token },
|
|
54599
|
+
body: JSON.stringify({ query: query2, variables })
|
|
54600
|
+
});
|
|
54601
|
+
} catch (err) {
|
|
54602
|
+
return { ok: false, text: `request error: ${err.message}` };
|
|
54603
|
+
}
|
|
54604
|
+
if (!resp.ok) {
|
|
54605
|
+
const txt = await resp.text().catch(() => "");
|
|
54606
|
+
return { ok: false, text: `Linear API ${resp.status}${txt ? ` \u2014 ${txt.slice(0, 200)}` : ""}` };
|
|
54607
|
+
}
|
|
54608
|
+
let json;
|
|
54609
|
+
try {
|
|
54610
|
+
json = await resp.json();
|
|
54611
|
+
} catch {
|
|
54612
|
+
return { ok: false, text: "malformed Linear API response" };
|
|
54613
|
+
}
|
|
54614
|
+
if (json.errors && json.errors.length > 0) {
|
|
54615
|
+
return { ok: false, text: json.errors.map((e) => e.message ?? "error").join("; ").slice(0, 300) };
|
|
54616
|
+
}
|
|
54617
|
+
return { ok: true, data: json.data };
|
|
54618
|
+
};
|
|
54619
|
+
if (dedupKey) {
|
|
54620
|
+
const search = await gql("query($term: String!) { searchIssues(term: $term) { nodes { id url title } } }", { term: dedupKey });
|
|
54621
|
+
if (search.ok) {
|
|
54622
|
+
const hit = (search.data?.searchIssues?.nodes ?? [])[0];
|
|
54623
|
+
if (hit?.url) {
|
|
54624
|
+
log(`telegram gateway: linear_create_issue: dedup hit key=${dedupKey} agent=${agent}
|
|
54625
|
+
`);
|
|
54626
|
+
return { content: [{ type: "text", text: `Already filed: ${hit.url}` }] };
|
|
54627
|
+
}
|
|
54628
|
+
}
|
|
54629
|
+
}
|
|
54630
|
+
let teamId = teamIdArg;
|
|
54631
|
+
if (!teamId) {
|
|
54632
|
+
const teams = await gql("query { teams(first: 50) { nodes { id key name } } }", {});
|
|
54633
|
+
if (!teams.ok) {
|
|
54634
|
+
return { content: [{ type: "text", text: `Couldn't file to Linear: ${teams.text}` }] };
|
|
54635
|
+
}
|
|
54636
|
+
const nodes = teams.data?.teams?.nodes ?? [];
|
|
54637
|
+
if (nodes.length === 0) {
|
|
54638
|
+
return { content: [{ type: "text", text: "Couldn't file to Linear: the workspace has no teams." }] };
|
|
54639
|
+
}
|
|
54640
|
+
if (nodes.length > 1) {
|
|
54641
|
+
const list2 = nodes.map((t) => `${t.key} (${t.name})`).join(", ");
|
|
54642
|
+
return {
|
|
54643
|
+
content: [
|
|
54644
|
+
{ 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.` }
|
|
54645
|
+
]
|
|
54646
|
+
};
|
|
54647
|
+
}
|
|
54648
|
+
teamId = nodes[0].id;
|
|
54649
|
+
}
|
|
54650
|
+
const description = dedupKey ? `${body}${captureDedupMarker(dedupKey)}` : body;
|
|
54651
|
+
const input = { teamId, title, description };
|
|
54652
|
+
if (priority !== undefined)
|
|
54653
|
+
input.priority = priority;
|
|
54654
|
+
const create = await gql("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url } } }", { input });
|
|
54655
|
+
if (!create.ok) {
|
|
54656
|
+
return { content: [{ type: "text", text: `Couldn't file to Linear: ${create.text}` }] };
|
|
54657
|
+
}
|
|
54658
|
+
const issue = create.data?.issueCreate?.issue;
|
|
54659
|
+
if (create.data?.issueCreate?.success === false || !issue?.url) {
|
|
54660
|
+
return { content: [{ type: "text", text: "Couldn't file to Linear: issue not created (success=false)." }] };
|
|
54661
|
+
}
|
|
54662
|
+
log(`telegram gateway: linear_create_issue: filed ${issue.identifier} agent=${agent}${dedupKey ? ` dedup=${dedupKey}` : ""}
|
|
54663
|
+
`);
|
|
54664
|
+
return { content: [{ type: "text", text: `Filed: ${title} \u2192 ${issue.url}` }] };
|
|
54665
|
+
}
|
|
54277
54666
|
|
|
54278
54667
|
// vault-approval-posture.ts
|
|
54279
54668
|
function resolveVaultApprovalPosture(broker) {
|
|
@@ -56049,6 +56438,8 @@ function sweepStaleAlwaysAllowCorrelations(now = Date.now()) {
|
|
|
56049
56438
|
}
|
|
56050
56439
|
}
|
|
56051
56440
|
}
|
|
56441
|
+
var scopedGrants = new Map;
|
|
56442
|
+
var selfAgentName = () => process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
56052
56443
|
var pendingAskUser = new Map;
|
|
56053
56444
|
var pendingReauthFlows = new Map;
|
|
56054
56445
|
var REAUTH_INTERCEPT_TTL_MS = 600000;
|
|
@@ -56283,6 +56674,7 @@ var pendingStateReaper = setInterval(() => {
|
|
|
56283
56674
|
if (now > v.expiresAt)
|
|
56284
56675
|
vaultPassphraseCache.delete(k);
|
|
56285
56676
|
}
|
|
56677
|
+
sweepScopedGrants(scopedGrants, now);
|
|
56286
56678
|
for (const [k, v] of deferredSecrets) {
|
|
56287
56679
|
if (now - v.staged_at > DEFERRED_SECRET_TTL_MS)
|
|
56288
56680
|
deferredSecrets.delete(k);
|
|
@@ -56822,8 +57214,10 @@ if (inboundSpool != null) {
|
|
|
56822
57214
|
}
|
|
56823
57215
|
}
|
|
56824
57216
|
var pendingPermissionBuffer = createPendingPermissionBuffer();
|
|
56825
|
-
function buildPermissionActionRow(requestId, showAlways) {
|
|
57217
|
+
function buildPermissionActionRow(requestId, showAlways, showTimeBox = false) {
|
|
56826
57218
|
const kb = new import_grammy9.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow once", `perm:allow:${requestId}`);
|
|
57219
|
+
if (showTimeBox)
|
|
57220
|
+
kb.text("\u23F1 30 min", `perm:tmb:${requestId}`);
|
|
56827
57221
|
if (showAlways)
|
|
56828
57222
|
kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
|
|
56829
57223
|
return kb;
|
|
@@ -57062,6 +57456,16 @@ var ipcServer = createIpcServer({
|
|
|
57062
57456
|
},
|
|
57063
57457
|
onPermissionRequest(_client, msg) {
|
|
57064
57458
|
const { requestId, toolName, description, inputPreview } = msg;
|
|
57459
|
+
const scopedTtl = scopedApprovalTtlMs();
|
|
57460
|
+
if (scopedTtl > 0) {
|
|
57461
|
+
const hit = lookupScopedGrant(scopedGrants, selfAgentName(), toolName, inputPreview, Date.now());
|
|
57462
|
+
if (hit) {
|
|
57463
|
+
dispatchPermissionVerdict({ type: "permission", requestId, behavior: "allow" });
|
|
57464
|
+
process.stderr.write(`telegram gateway: scoped-approval auto-allow tool=${toolName} rule="${hit}" request=${requestId} (time-boxed window)
|
|
57465
|
+
`);
|
|
57466
|
+
return;
|
|
57467
|
+
}
|
|
57468
|
+
}
|
|
57065
57469
|
pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now() });
|
|
57066
57470
|
const text = formatPermissionCardBody({
|
|
57067
57471
|
toolName,
|
|
@@ -57069,8 +57473,10 @@ var ipcServer = createIpcServer({
|
|
|
57069
57473
|
description,
|
|
57070
57474
|
agentName: _client.agentName
|
|
57071
57475
|
});
|
|
57072
|
-
const
|
|
57073
|
-
const
|
|
57476
|
+
const scopeChoices = resolveScopedAllowChoices(toolName, inputPreview);
|
|
57477
|
+
const showAlways = scopeChoices != null;
|
|
57478
|
+
const showTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(toolName, inputPreview, scopeChoices) != null;
|
|
57479
|
+
const keyboard = buildPermissionActionRow(requestId, showAlways, showTimeBox);
|
|
57074
57480
|
const activeTurn = currentTurn;
|
|
57075
57481
|
const targets = resolvePermissionCardTargets();
|
|
57076
57482
|
for (const { chatId, threadId } of targets) {
|
|
@@ -57359,6 +57765,7 @@ var ipcServer = createIpcServer({
|
|
|
57359
57765
|
if (fellBackToMain) {
|
|
57360
57766
|
process.stderr.write(`telegram gateway: cron fire fell back to main session (no cron bridge) agent=${msg.agentName} prompt_key=${promptKey}
|
|
57361
57767
|
`);
|
|
57768
|
+
emitRuntimeMetric({ kind: "cron_fell_back_to_main", agent: msg.agentName, prompt_key: promptKey });
|
|
57362
57769
|
}
|
|
57363
57770
|
if (delivered && target === msg.agentName)
|
|
57364
57771
|
markClaudeBusyForInbound(msg.inbound);
|
|
@@ -57368,6 +57775,29 @@ var ipcServer = createIpcServer({
|
|
|
57368
57775
|
pendingInboundBuffer.push(target, msg.inbound);
|
|
57369
57776
|
}
|
|
57370
57777
|
},
|
|
57778
|
+
onSendOutbound(_client, msg) {
|
|
57779
|
+
const self2 = process.env.SWITCHROOM_AGENT_NAME;
|
|
57780
|
+
if (self2 && msg.agentName !== self2) {
|
|
57781
|
+
process.stderr.write(`telegram gateway: send_outbound rejected \u2014 agent mismatch (${msg.agentName} != ${self2})
|
|
57782
|
+
`);
|
|
57783
|
+
return;
|
|
57784
|
+
}
|
|
57785
|
+
try {
|
|
57786
|
+
assertAllowedChat(msg.chatId);
|
|
57787
|
+
} catch (err) {
|
|
57788
|
+
process.stderr.write(`telegram gateway: send_outbound rejected \u2014 ${err.message}
|
|
57789
|
+
`);
|
|
57790
|
+
return;
|
|
57791
|
+
}
|
|
57792
|
+
const threadId = msg.threadId;
|
|
57793
|
+
const parseMode = msg.parseMode === "text" ? undefined : "HTML";
|
|
57794
|
+
swallowingApiCall(() => bot.api.sendMessage(msg.chatId, msg.text, {
|
|
57795
|
+
...parseMode ? { parse_mode: parseMode } : {},
|
|
57796
|
+
...threadId != null && threadId !== 1 ? { message_thread_id: threadId } : {}
|
|
57797
|
+
}), { chat_id: msg.chatId, verb: "cron-action-send", ...threadId != null ? { threadId } : {} });
|
|
57798
|
+
process.stderr.write(`telegram gateway: send_outbound agent=${msg.agentName} chat=${msg.chatId} thread=${threadId ?? "-"} len=${msg.text.length}
|
|
57799
|
+
`);
|
|
57800
|
+
},
|
|
57371
57801
|
onQuotaWallDetected(_client, msg) {
|
|
57372
57802
|
const untilMs = resolveExhaustUntil(msg.resetAt);
|
|
57373
57803
|
process.stderr.write(`telegram gateway: quota_wall_detected agent=${msg.agentName} until=${new Date(untilMs).toISOString()}` + (msg.resetAt == null ? " (reset unparsed \u2192 +7d default)" : "") + ` \u2014 triggering fleet auto-fallback
|
|
@@ -57483,7 +57913,8 @@ var ALLOWED_TOOLS = new Set([
|
|
|
57483
57913
|
"vault_request_save",
|
|
57484
57914
|
"vault_request_access",
|
|
57485
57915
|
"request_secret",
|
|
57486
|
-
"linear_agent_activity"
|
|
57916
|
+
"linear_agent_activity",
|
|
57917
|
+
"linear_create_issue"
|
|
57487
57918
|
]);
|
|
57488
57919
|
async function executeToolCall(tool, args) {
|
|
57489
57920
|
if (!ALLOWED_TOOLS.has(tool)) {
|
|
@@ -57530,6 +57961,8 @@ async function executeToolCall(tool, args) {
|
|
|
57530
57961
|
return executeRequestSecret(args);
|
|
57531
57962
|
case "linear_agent_activity":
|
|
57532
57963
|
return executeLinearAgentActivity(args);
|
|
57964
|
+
case "linear_create_issue":
|
|
57965
|
+
return executeLinearCreateIssue(args);
|
|
57533
57966
|
default:
|
|
57534
57967
|
throw new Error(`unknown tool: ${tool}`);
|
|
57535
57968
|
}
|
|
@@ -57563,6 +57996,9 @@ async function executeSendChecklist(args) {
|
|
|
57563
57996
|
async function executeLinearAgentActivity(args) {
|
|
57564
57997
|
return emitLinearAgentActivity(args);
|
|
57565
57998
|
}
|
|
57999
|
+
async function executeLinearCreateIssue(args) {
|
|
58000
|
+
return createLinearIssue(args);
|
|
58001
|
+
}
|
|
57566
58002
|
async function executeUpdateChecklist(args) {
|
|
57567
58003
|
const chat_id = args.chat_id;
|
|
57568
58004
|
if (!chat_id)
|
|
@@ -60938,7 +61374,7 @@ function getMyAgentName() {
|
|
|
60938
61374
|
const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
|
|
60939
61375
|
if (fromEnv && fromEnv.trim().length > 0)
|
|
60940
61376
|
return fromEnv.trim();
|
|
60941
|
-
return
|
|
61377
|
+
return basename9(process.cwd());
|
|
60942
61378
|
}
|
|
60943
61379
|
function isSelfTargetingCommand(name) {
|
|
60944
61380
|
if (name === "all")
|
|
@@ -64722,7 +65158,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
64722
65158
|
}
|
|
64723
65159
|
return;
|
|
64724
65160
|
}
|
|
64725
|
-
const m = /^perm:(allow|deny|always|asn|asb|back):([a-km-z]{5})$/.exec(data);
|
|
65161
|
+
const m = /^perm:(allow|deny|always|asn|asb|back|tmb):([a-km-z]{5})$/.exec(data);
|
|
64726
65162
|
if (!m) {
|
|
64727
65163
|
await ctx.answerCallbackQuery().catch(() => {});
|
|
64728
65164
|
return;
|
|
@@ -64742,7 +65178,9 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
64742
65178
|
}
|
|
64743
65179
|
let keyboard;
|
|
64744
65180
|
if (behavior === "back") {
|
|
64745
|
-
|
|
65181
|
+
const backChoices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
65182
|
+
const backTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(details.tool_name, details.input_preview, backChoices) != null;
|
|
65183
|
+
keyboard = buildPermissionActionRow(request_id, true, backTimeBox);
|
|
64746
65184
|
} else {
|
|
64747
65185
|
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
64748
65186
|
if (choices == null) {
|
|
@@ -64883,6 +65321,51 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
64883
65321
|
ackText: ackText.slice(0, 200),
|
|
64884
65322
|
newText: baseText2 ? `${baseText2}
|
|
64885
65323
|
|
|
65324
|
+
${editLabel}` : editLabel,
|
|
65325
|
+
parseMode: "HTML"
|
|
65326
|
+
});
|
|
65327
|
+
return;
|
|
65328
|
+
}
|
|
65329
|
+
if (behavior === "tmb") {
|
|
65330
|
+
const details = pendingPermissions.get(request_id);
|
|
65331
|
+
if (!details) {
|
|
65332
|
+
await ctx.answerCallbackQuery({ text: "Details no longer available." }).catch(() => {});
|
|
65333
|
+
return;
|
|
65334
|
+
}
|
|
65335
|
+
const ttl = scopedApprovalTtlMs();
|
|
65336
|
+
if (ttl <= 0) {
|
|
65337
|
+
await ctx.answerCallbackQuery({ text: "Time-boxed approvals are disabled." }).catch(() => {});
|
|
65338
|
+
return;
|
|
65339
|
+
}
|
|
65340
|
+
const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
|
|
65341
|
+
const tb = resolveTimeBox(details.tool_name, details.input_preview, choices);
|
|
65342
|
+
if (!tb) {
|
|
65343
|
+
await ctx.answerCallbackQuery({ text: "This action can't be time-boxed." }).catch(() => {});
|
|
65344
|
+
return;
|
|
65345
|
+
}
|
|
65346
|
+
const agentName3 = selfAgentName();
|
|
65347
|
+
if (!agentName3) {
|
|
65348
|
+
await ctx.answerCallbackQuery({ text: "Time-box needs SWITCHROOM_AGENT_NAME \u2014 gateway is misconfigured." }).catch(() => {});
|
|
65349
|
+
return;
|
|
65350
|
+
}
|
|
65351
|
+
pendingPermissions.delete(request_id);
|
|
65352
|
+
dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior: "allow" });
|
|
65353
|
+
recordScopedGrant(scopedGrants, agentName3, tb.rule, Date.now(), ttl);
|
|
65354
|
+
resumeReactionAfterVerdict();
|
|
65355
|
+
postPermissionResumeMessage({
|
|
65356
|
+
behavior: "allow",
|
|
65357
|
+
action: naturalAction(details.tool_name, details.input_preview)
|
|
65358
|
+
});
|
|
65359
|
+
process.stderr.write(`telegram gateway: scoped-approval granted rule="${tb.rule}" agent=${agentName3} ttl_ms=${ttl} (request_id=${request_id})
|
|
65360
|
+
`);
|
|
65361
|
+
const mins = Math.max(1, Math.round(ttl / 60000));
|
|
65362
|
+
const sourceMsg = ctx.callbackQuery?.message;
|
|
65363
|
+
const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
|
|
65364
|
+
const editLabel = `\u23F1 <b>Allowed for ${mins} min \u2014 ${escapeHtmlForTg(tb.breadth)}</b> \xB7 re-asks after that, and now for anything else`;
|
|
65365
|
+
await finalizeCallback(ctx, {
|
|
65366
|
+
ackText: `\u23F1 Allowed for ${mins} min`.slice(0, 200),
|
|
65367
|
+
newText: baseText2 ? `${baseText2}
|
|
65368
|
+
|
|
64886
65369
|
${editLabel}` : editLabel,
|
|
64887
65370
|
parseMode: "HTML"
|
|
64888
65371
|
});
|