switchroom 0.18.12 → 0.18.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 +8 -0
- package/dist/auth-broker/index.js +63 -65
- package/dist/cli/ms-365-write-pretool.mjs +31 -8
- package/dist/cli/notion-write-pretool.mjs +9 -1
- package/dist/cli/skill-validate-pretool.mjs +144 -2847
- package/dist/cli/switchroom.js +952 -3126
- package/dist/host-control/main.js +216 -2862
- package/dist/vault/approvals/kernel-server.js +67 -0
- package/dist/vault/broker/server.js +98 -44
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +49 -3
- package/telegram-plugin/dist/gateway/gateway.js +656 -2326
- package/telegram-plugin/dist/server.js +65 -3
- package/telegram-plugin/format.ts +19 -0
- package/telegram-plugin/gateway/approval-hold.ts +21 -2
- package/telegram-plugin/gateway/callback-query-handlers.ts +12 -0
- package/telegram-plugin/gateway/gateway.ts +221 -73
- package/telegram-plugin/history.ts +51 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +94 -0
- package/telegram-plugin/model-unavailable.ts +41 -11
- package/telegram-plugin/outbound-field-redact.ts +69 -0
- package/telegram-plugin/render/render.ts +32 -14
- package/telegram-plugin/scoped-approval.ts +11 -2
- package/telegram-plugin/secret-detect/chunker.ts +18 -4
- package/telegram-plugin/secret-detect/index.ts +12 -56
- package/telegram-plugin/send-gate-degraded.test.ts +131 -0
- package/telegram-plugin/send-gate.test.ts +25 -6
- package/telegram-plugin/send-gate.ts +82 -8
- package/telegram-plugin/session-tail.ts +82 -7
- package/telegram-plugin/subagent-watcher.ts +71 -16
- package/telegram-plugin/tests/approval-hold-outcome.test.ts +36 -5
- package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
- package/telegram-plugin/tests/history.test.ts +115 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +164 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
- package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
- package/telegram-plugin/tests/render/render.test.ts +88 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
- package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
- package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
- package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
- package/telegram-plugin/worktree-watch-cwds.ts +194 -5
- package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
- package/telegram-plugin/tests/secret-detect-secretlint.test.ts +0 -105
|
@@ -17062,6 +17062,48 @@ var init_operator_events = __esm(() => {
|
|
|
17062
17062
|
cooldownMap = new Map;
|
|
17063
17063
|
});
|
|
17064
17064
|
|
|
17065
|
+
// quota-check.ts
|
|
17066
|
+
var init_quota_check = () => {};
|
|
17067
|
+
|
|
17068
|
+
// text-voice-scrub.ts
|
|
17069
|
+
var NULL = "\x00", FENCE_PH, INLINE_PH, HTML_CODE_PH, HTML_PRE_PH, URL_PH;
|
|
17070
|
+
var init_text_voice_scrub = __esm(() => {
|
|
17071
|
+
FENCE_PH = `${NULL}VS_FENCE`;
|
|
17072
|
+
INLINE_PH = `${NULL}VS_INLINE`;
|
|
17073
|
+
HTML_CODE_PH = `${NULL}VS_HTMLCODE`;
|
|
17074
|
+
HTML_PRE_PH = `${NULL}VS_HTMLPRE`;
|
|
17075
|
+
URL_PH = `${NULL}VS_URL`;
|
|
17076
|
+
});
|
|
17077
|
+
|
|
17078
|
+
// card-format.ts
|
|
17079
|
+
var init_card_format = __esm(() => {
|
|
17080
|
+
init_text_voice_scrub();
|
|
17081
|
+
});
|
|
17082
|
+
|
|
17083
|
+
// model-unavailable.ts
|
|
17084
|
+
function isTransientUpstreamSignal(text) {
|
|
17085
|
+
if (typeof text !== "string" || text.length === 0)
|
|
17086
|
+
return false;
|
|
17087
|
+
const sample = text.length > 16384 ? text.slice(0, 16384) : text;
|
|
17088
|
+
const lower = sample.toLowerCase();
|
|
17089
|
+
return transientUpstreamSignals.some((s) => lower.includes(s));
|
|
17090
|
+
}
|
|
17091
|
+
var transientUpstreamSignals;
|
|
17092
|
+
var init_model_unavailable = __esm(() => {
|
|
17093
|
+
init_quota_check();
|
|
17094
|
+
init_card_format();
|
|
17095
|
+
transientUpstreamSignals = [
|
|
17096
|
+
"not your usage limit",
|
|
17097
|
+
"not your account",
|
|
17098
|
+
"not your account's",
|
|
17099
|
+
"temporarily limiting requests",
|
|
17100
|
+
"temporarily rate",
|
|
17101
|
+
"server is temporarily",
|
|
17102
|
+
"would exceed your account\u2019s rate limit",
|
|
17103
|
+
"would exceed your account's rate limit"
|
|
17104
|
+
];
|
|
17105
|
+
});
|
|
17106
|
+
|
|
17065
17107
|
// tool-label-sidecar.ts
|
|
17066
17108
|
import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
|
17067
17109
|
import { join as join3 } from "node:path";
|
|
@@ -17491,7 +17533,8 @@ function detectErrorInTranscriptLine(line) {
|
|
|
17491
17533
|
const status = typeof obj.apiErrorStatus === "number" ? obj.apiErrorStatus : null;
|
|
17492
17534
|
const errStr = typeof obj.error === "string" ? obj.error : "";
|
|
17493
17535
|
const text = extractAssistantText(obj);
|
|
17494
|
-
const kind2 = status === 429 ?
|
|
17536
|
+
const kind2 = status === 429 ? isTransientUpstreamSignal(`${text}
|
|
17537
|
+
${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: errStr, status, message: text });
|
|
17495
17538
|
return {
|
|
17496
17539
|
kind: kind2,
|
|
17497
17540
|
raw: obj,
|
|
@@ -17585,6 +17628,7 @@ function startSessionTail(config2) {
|
|
|
17585
17628
|
const onOperatorEvent = config2.onOperatorEvent;
|
|
17586
17629
|
log?.(`session-tail: projectsDir=${projectsDir}`);
|
|
17587
17630
|
const sidecars = new Map;
|
|
17631
|
+
const createSidecar = config2.createSidecar ?? createToolLabelSidecar;
|
|
17588
17632
|
const stateDirForSidecar = process.env.TELEGRAM_STATE_DIR ?? null;
|
|
17589
17633
|
function sessionIdForFile(file) {
|
|
17590
17634
|
if (!file)
|
|
@@ -17599,7 +17643,7 @@ function startSessionTail(config2) {
|
|
|
17599
17643
|
if (existing)
|
|
17600
17644
|
return existing;
|
|
17601
17645
|
try {
|
|
17602
|
-
const s =
|
|
17646
|
+
const s = createSidecar({ stateDir: stateDirForSidecar, sessionId });
|
|
17603
17647
|
sidecars.set(sessionId, s);
|
|
17604
17648
|
s.onLabel((toolUseId, label, toolName) => {
|
|
17605
17649
|
rawOnEvent({ kind: "tool_label", toolUseId, label, toolName });
|
|
@@ -17610,6 +17654,17 @@ function startSessionTail(config2) {
|
|
|
17610
17654
|
return null;
|
|
17611
17655
|
}
|
|
17612
17656
|
}
|
|
17657
|
+
function stopSidecar(sessionId) {
|
|
17658
|
+
if (!sessionId)
|
|
17659
|
+
return;
|
|
17660
|
+
const s = sidecars.get(sessionId);
|
|
17661
|
+
if (!s)
|
|
17662
|
+
return;
|
|
17663
|
+
try {
|
|
17664
|
+
s.stop();
|
|
17665
|
+
} catch {}
|
|
17666
|
+
sidecars.delete(sessionId);
|
|
17667
|
+
}
|
|
17613
17668
|
function decorate(ev, sessionId) {
|
|
17614
17669
|
if (!sessionId)
|
|
17615
17670
|
return ev;
|
|
@@ -17702,6 +17757,11 @@ function startSessionTail(config2) {
|
|
|
17702
17757
|
} catch {}
|
|
17703
17758
|
watcher = null;
|
|
17704
17759
|
}
|
|
17760
|
+
const rotatedAwaySid = sessionIdForFile(currentFile);
|
|
17761
|
+
const nextSid = sessionIdForFile(file);
|
|
17762
|
+
if (rotatedAwaySid != null && rotatedAwaySid !== nextSid) {
|
|
17763
|
+
stopSidecar(rotatedAwaySid);
|
|
17764
|
+
}
|
|
17705
17765
|
currentFile = file;
|
|
17706
17766
|
const prior = fileCursors.get(file);
|
|
17707
17767
|
if (prior != null) {
|
|
@@ -17734,7 +17794,7 @@ function startSessionTail(config2) {
|
|
|
17734
17794
|
const multiAgent = isMultiAgentEnabled();
|
|
17735
17795
|
const CAP_TOOL_USE_THRESHOLD = 30;
|
|
17736
17796
|
const subTails = new Map;
|
|
17737
|
-
const IDLE_FSWATCH_TTL_MS = 5 * 60 * 1000;
|
|
17797
|
+
const IDLE_FSWATCH_TTL_MS = Math.max(1000, config2.subTailIdleReapMs ?? 5 * 60 * 1000);
|
|
17738
17798
|
function readSub(t) {
|
|
17739
17799
|
if (stopped)
|
|
17740
17800
|
return;
|
|
@@ -17841,6 +17901,7 @@ function startSessionTail(config2) {
|
|
|
17841
17901
|
} catch {}
|
|
17842
17902
|
t.watcher = null;
|
|
17843
17903
|
}
|
|
17904
|
+
stopSidecar(sessionIdForFile(t.file));
|
|
17844
17905
|
subTails.delete(file);
|
|
17845
17906
|
log?.(`session-tail: reaped idle sub ${t.agentId} (${file})`);
|
|
17846
17907
|
}
|
|
@@ -17925,6 +17986,7 @@ function startSessionTail(config2) {
|
|
|
17925
17986
|
var MAX_JSONL_LINE_BYTES, MAX_ERROR_TEXT_CHARS = 500;
|
|
17926
17987
|
var init_session_tail = __esm(() => {
|
|
17927
17988
|
init_operator_events();
|
|
17989
|
+
init_model_unavailable();
|
|
17928
17990
|
init_tool_label_sidecar();
|
|
17929
17991
|
init_model_label();
|
|
17930
17992
|
MAX_JSONL_LINE_BYTES = 2 * 1024 * 1024;
|
|
@@ -71,6 +71,25 @@ export function codeSpanSafe(s: string): string {
|
|
|
71
71
|
return s.replace(/`/g, '`')
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Make a URL safe to interpolate as the destination of a `[label](href)`
|
|
76
|
+
* inline link.
|
|
77
|
+
*
|
|
78
|
+
* In GFM / Bot API 10.1 markdown a link destination written as `(...)` is a
|
|
79
|
+
* bare destination whose parentheses must be BALANCED, or every paren must be
|
|
80
|
+
* backslash-escaped. An href containing a literal `)` (Wikipedia
|
|
81
|
+
* `..._(disambiguation)` URLs, tracking params, generated deep links) can
|
|
82
|
+
* either truncate the URL (a lone `)` closing the destination) or, if we
|
|
83
|
+
* escape only `)`, unbalance the parens and leak a literal backslash into the
|
|
84
|
+
* decoded href. The destination honours C-style backslash escapes, so escape
|
|
85
|
+
* `\` first (so we never double-escape a following escape), then BOTH `(` and
|
|
86
|
+
* `)` — the whole URL is preserved balanced and micromark decodes it back to
|
|
87
|
+
* the original href on round-trip. Bot API 10.1 lists `(`/`)` as escapable.
|
|
88
|
+
*/
|
|
89
|
+
export function escapeLinkHref(href: string): string {
|
|
90
|
+
return href.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)')
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
/**
|
|
75
94
|
* Repair LLM-side JSON escape bungles.
|
|
76
95
|
*
|
|
@@ -268,12 +268,31 @@ export function createBlockedApprovalStore(
|
|
|
268
268
|
*
|
|
269
269
|
* This exists so the feature cannot silently no-op. If the shared bind is
|
|
270
270
|
* missing (a container predating the volume), or auto-created root-owned, the
|
|
271
|
-
* record still lands somewhere the operator and switchroom-web can read
|
|
272
|
-
*
|
|
271
|
+
* record still lands somewhere the operator and switchroom-web can read.
|
|
272
|
+
*
|
|
273
|
+
* **Reachable by mount is not the same as readable, and neither is the same as
|
|
274
|
+
* READ.** That elision is a bug this comment used to paper over: it claimed
|
|
275
|
+
* "web mounts all of ~/.switchroom, so both locations are reachable" — true,
|
|
276
|
+
* and irrelevant, because the reader only ever scanned the shared dir. The
|
|
277
|
+
* fallback was written to a file nobody read (#3109). Two things must hold, and
|
|
278
|
+
* both are load-bearing: if either regresses, the record exists, the agent is
|
|
279
|
+
* held, and the dashboard still prints "No agent is blocked".
|
|
280
|
+
*
|
|
281
|
+
* 1. **Mode.** The record is written 0644 into the 0775 agent dir. Verified
|
|
282
|
+
* live: `docker exec switchroom-web` reads a 0644 file under
|
|
283
|
+
* `~/.switchroom/agents/<agent>/` fine and gets EACCES only on the 0600
|
|
284
|
+
* files beside it. The FILE's mode locks web out, never the directory.
|
|
285
|
+
* 2. **The reader must LOOK here.** It now does —
|
|
286
|
+
* `src/web/blocked-approvals-read.ts` scans the shared dir AND
|
|
287
|
+
* `agents/<agent>/blocked-approval.json` (its `FALLBACK_RECORD_NAME`,
|
|
288
|
+
* which must stay in sync with the filename below).
|
|
273
289
|
*/
|
|
274
290
|
fallbackDir?: string,
|
|
275
291
|
): BlockedApprovalStore {
|
|
276
292
|
const primary = join(dir, `${agent}.json`)
|
|
293
|
+
// Keep in sync with FALLBACK_RECORD_NAME in src/web/blocked-approvals-read.ts —
|
|
294
|
+
// the reader matches this filename exactly. Pinned by the fallback-contract
|
|
295
|
+
// tests in src/web/blocked-approvals.test.ts.
|
|
277
296
|
const fallback = fallbackDir != null ? join(fallbackDir, 'blocked-approval.json') : null
|
|
278
297
|
|
|
279
298
|
/** Where the last successful write landed — `read`/`clear` must agree with it. */
|
|
@@ -2534,6 +2534,18 @@ async function handleOperatorEventCallback(ctx: Context, data: string): Promise<
|
|
|
2534
2534
|
// stub so any stale pinned message that fires an `auth:*` tap is
|
|
2535
2535
|
// silently dismissed instead of crashing the gateway.
|
|
2536
2536
|
async function handleAuthDashboardCallback(ctx: Context): Promise<void> {
|
|
2537
|
+
// Strict allowFrom gate, identical to every other mutating handler in
|
|
2538
|
+
// this file (handleOperatorEventCallback, the vra:/vrs:/vd:/vg: families).
|
|
2539
|
+
// Its absence was a security hole: `auth:use:<label>` drives
|
|
2540
|
+
// `client.setActive(label)` — a fleet-wide OAuth account swap — so an
|
|
2541
|
+
// ungated handler let any tapper (e.g. a member of an admin forum/
|
|
2542
|
+
// supergroup with an empty group allowFrom) swap the active account.
|
|
2543
|
+
const senderId = String(ctx.from?.id ?? '')
|
|
2544
|
+
const access = loadAccess()
|
|
2545
|
+
if (!access.allowFrom.includes(senderId)) {
|
|
2546
|
+
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
|
2547
|
+
return
|
|
2548
|
+
}
|
|
2537
2549
|
const data = ctx.callbackQuery?.data ?? ''
|
|
2538
2550
|
const currentAgent = getMyAgentName()
|
|
2539
2551
|
|