switchroom 0.15.24 → 0.15.25

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.
@@ -50477,8 +50477,8 @@ var {
50477
50477
  } = import__.default;
50478
50478
 
50479
50479
  // src/build-info.ts
50480
- var VERSION = "0.15.24";
50481
- var COMMIT_SHA = "3fce3b89";
50480
+ var VERSION = "0.15.25";
50481
+ var COMMIT_SHA = "0d066743";
50482
50482
 
50483
50483
  // src/cli/agent.ts
50484
50484
  init_source();
@@ -52885,7 +52885,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
52885
52885
  const tools = agentConfig.tools ?? { allow: [], deny: [] };
52886
52886
  const rawAllow = tools.allow ?? [];
52887
52887
  const hasAllWildcard = rawAllow.includes("all");
52888
- const baseAllow = hasAllWildcard ? ALL_BUILTIN_TOOLS : rawAllow.filter((t) => t !== "all");
52888
+ const baseAllow = hasAllWildcard ? [...ALL_BUILTIN_TOOLS, ...rawAllow.filter((t) => t !== "all")] : rawAllow.filter((t) => t !== "all");
52889
52889
  const dangerousMode = agentConfig.dangerous_mode === true;
52890
52890
  const hadExplicitAllow = rawAllow.length > 0;
52891
52891
  const readOnlyDefaults = !dangerousMode && !hadExplicitAllow ? DEFAULT_READ_ONLY_PREAPPROVED_TOOLS : [];
@@ -53719,7 +53719,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
53719
53719
  const tools = agentConfig.tools ?? { allow: [], deny: [] };
53720
53720
  const rawAllow = tools.allow ?? [];
53721
53721
  const hasAllWildcard = rawAllow.includes("all");
53722
- const baseAllow = hasAllWildcard ? ALL_BUILTIN_TOOLS : rawAllow.filter((t) => t !== "all");
53722
+ const baseAllow = hasAllWildcard ? [...ALL_BUILTIN_TOOLS, ...rawAllow.filter((t) => t !== "all")] : rawAllow.filter((t) => t !== "all");
53723
53723
  const reconcileDangerousMode = agentConfig.dangerous_mode === true;
53724
53724
  const reconcileHadExplicitAllow = rawAllow.length > 0;
53725
53725
  const reconcileReadOnlyDefaults = !reconcileDangerousMode && !reconcileHadExplicitAllow ? DEFAULT_READ_ONLY_PREAPPROVED_TOOLS : [];
@@ -82565,6 +82565,7 @@ Applying switchroom config...
82565
82565
  process.exit(6);
82566
82566
  }
82567
82567
  const composePath = options.outPath ?? DEFAULT_COMPOSE_PATH2;
82568
+ const displayComposePath = toHostHomePath(composePath);
82568
82569
  const operatorUid = resolveOperatorUid();
82569
82570
  const { bytes: composeBytes } = await writeComposeFile({
82570
82571
  config,
@@ -82575,11 +82576,11 @@ Applying switchroom config...
82575
82576
  buildContext: options.buildContext
82576
82577
  });
82577
82578
  writeOut(source_default.bold(`
82578
- Wrote `) + composePath + source_default.gray(` (${composeBytes} bytes)
82579
+ Wrote `) + displayComposePath + source_default.gray(` (${composeBytes} bytes)
82579
82580
  `));
82580
82581
  writeOut(`Bring the fleet up with:
82581
- ` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${composePath} pull && \\
82582
- ` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${composePath} up -d --remove-orphans
82582
+ ` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} pull && \\
82583
+ ` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} up -d --remove-orphans
82583
82584
  `);
82584
82585
  writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
82585
82586
  `));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.24",
3
+ "version": "0.15.25",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53639,7 +53639,11 @@ function scopedApprovalTtlMs(env = process.env) {
53639
53639
  }
53640
53640
  var FILE_RULE = /^(Edit|Write|MultiEdit|NotebookEdit|Read)\((.+)\)$/;
53641
53641
  var BASH_FAMILY_RULE = /^Bash\(([^:]+):\*\)$/;
53642
+ var READ_ONLY_WHOLE_TOOLS = new Set(["Grep", "Glob"]);
53642
53643
  function resolveTimeBox(toolName, inputPreview, choices) {
53644
+ if (READ_ONLY_WHOLE_TOOLS.has(toolName) && choices?.broad) {
53645
+ return { rule: choices.broad.rule, breadth: `any ${toolName}` };
53646
+ }
53643
53647
  const specific = choices?.specific;
53644
53648
  if (!specific || specific.broad)
53645
53649
  return null;
@@ -54420,10 +54424,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54420
54424
  }
54421
54425
 
54422
54426
  // ../src/build-info.ts
54423
- var VERSION = "0.15.24";
54424
- var COMMIT_SHA = "3fce3b89";
54425
- var COMMIT_DATE = "2026-06-14T21:03:53Z";
54426
- var LATEST_PR = 2354;
54427
+ var VERSION = "0.15.25";
54428
+ var COMMIT_SHA = "0d066743";
54429
+ var COMMIT_DATE = "2026-06-15T02:15:36Z";
54430
+ var LATEST_PR = 2359;
54427
54431
  var COMMITS_AHEAD_OF_TAG = 0;
54428
54432
 
54429
54433
  // gateway/boot-version.ts
@@ -83,6 +83,16 @@ export interface TimeBoxDecision {
83
83
  const FILE_RULE = /^(Edit|Write|MultiEdit|NotebookEdit|Read)\((.+)\)$/;
84
84
  const BASH_FAMILY_RULE = /^Bash\(([^:]+):\*\)$/;
85
85
 
86
+ // Read-only whole-tool inspection tools. permission-rule.ts classifies these
87
+ // as BROAD_ONLY (no meaningful path sub-scope — Grep/Glob match a pattern
88
+ // across files), so they only ever offer a broad "any Grep/Glob" grant. They
89
+ // are READ-ONLY — they cannot mutate anything — so time-boxing that broad grant
90
+ // is low-risk and is exactly the dominant "stop re-asking for the same
91
+ // low-risk inspection" case (e.g. grepping a file with several regexes). This
92
+ // deliberately EXCLUDES WebFetch/WebSearch (also BROAD_ONLY): network egress is
93
+ // a different risk class, and webkite denies them anyway.
94
+ const READ_ONLY_WHOLE_TOOLS = new Set(["Grep", "Glob"]);
95
+
86
96
  /**
87
97
  * Conservative time-box eligibility. Given the already-resolved scope
88
98
  * choices for a permission request, return the NARROW rule to time-box
@@ -96,12 +106,24 @@ const BASH_FAMILY_RULE = /^Bash\(([^:]+):\*\)$/;
96
106
  * triggering command itself is non-destructive. The family grant
97
107
  * still covers the whole `<tok>` family for matching, but match-time
98
108
  * re-checks each later command (see `lookupScopedGrant`).
109
+ * - Read-only whole-tool inspection (Grep/Glob) → time-boxable on the
110
+ * broad grant: no sub-scope exists, but they cannot mutate anything, so
111
+ * "any Grep for 30 min" is the safe, dominant low-risk-repeat case.
99
112
  */
100
113
  export function resolveTimeBox(
101
114
  toolName: string,
102
115
  inputPreview: string | undefined,
103
116
  choices: ScopedAllowChoices | null,
104
117
  ): TimeBoxDecision | null {
118
+ // Read-only whole-tool inspection (Grep/Glob) has no narrow sub-scope, only
119
+ // a broad grant — but it is read-only, so the broad grant is safe to
120
+ // time-box and is the dominant low-risk-repeat case. A stored bare-tool
121
+ // rule ("Grep") matches every later Grep call (matchesAllowRule: rule ===
122
+ // toolName), so different regexes/paths all dedup for the window.
123
+ if (READ_ONLY_WHOLE_TOOLS.has(toolName) && choices?.broad) {
124
+ return { rule: choices.broad.rule, breadth: `any ${toolName}` };
125
+ }
126
+
105
127
  // Only ever time-box the narrow scope, and only when one exists.
106
128
  const specific = choices?.specific;
107
129
  if (!specific || specific.broad) return null;
@@ -100,6 +100,26 @@ describe('resolveTimeBox — conservative eligibility', () => {
100
100
  expect(timeBoxRule('Skill', JSON.stringify({ skill: 'deep-research' }))).toBeNull()
101
101
  expect(timeBoxRule('TotallyUnknown', '{}')).toBeNull()
102
102
  })
103
+
104
+ it('time-boxes read-only whole-tool inspection (Grep/Glob) on the broad grant', () => {
105
+ // No narrow sub-scope exists (BROAD_ONLY), but they are read-only — so the
106
+ // broad grant is safe to time-box. This is the dominant "stop re-asking for
107
+ // the same low-risk inspection" case the operator wants.
108
+ expect(timeBoxRule('Grep', JSON.stringify({ pattern: 'foo', path: '/state/x.ts' }))).toBe('Grep')
109
+ expect(timeBoxRule('Glob', JSON.stringify({ pattern: '**/*.ts' }))).toBe('Glob')
110
+ })
111
+
112
+ it('honest breadth for the read-only whole-tool window', () => {
113
+ const choices = resolveScopedAllowChoices('Grep', JSON.stringify({ pattern: 'foo' }))
114
+ expect(resolveTimeBox('Grep', JSON.stringify({ pattern: 'foo' }), choices)?.breadth).toBe('any Grep')
115
+ })
116
+
117
+ it('still does NOT time-box network-egress broad-only tools (WebFetch/WebSearch)', () => {
118
+ // Read-only-of-the-filesystem is the bar; network egress is a different
119
+ // risk class and is excluded on purpose (also denied via webkite).
120
+ expect(timeBoxRule('WebFetch', JSON.stringify({ url: 'https://x' }))).toBeNull()
121
+ expect(timeBoxRule('WebSearch', JSON.stringify({ query: 'x' }))).toBeNull()
122
+ })
103
123
  })
104
124
 
105
125
  describe('lookupScopedGrant — no seed, no extend, fail closed', () => {
@@ -121,6 +141,19 @@ describe('lookupScopedGrant — no seed, no extend, fail closed', () => {
121
141
  expect(lookupScopedGrant(store, 'clerk', 'Edit', editInput('/state/y.ts'), T0 + 1)).toBeNull()
122
142
  })
123
143
 
144
+ it('a Grep grant dedups later Greps with DIFFERENT regexes (the spam case)', () => {
145
+ // Operator taps ✅ Allow on the first Grep → whole-tool window. The agent
146
+ // then greps the same file with 2 more regexes — both auto-allow, no
147
+ // re-prompt, for the life of the window.
148
+ const store: ScopedGrantStore = new Map()
149
+ const t = resolveTimeBox('Grep', JSON.stringify({ pattern: 'foo' }), resolveScopedAllowChoices('Grep', JSON.stringify({ pattern: 'foo' })))
150
+ recordScopedGrant(store, 'clerk', t!.rule, T0, TTL)
151
+ expect(lookupScopedGrant(store, 'clerk', 'Grep', JSON.stringify({ pattern: 'bar' }), T0 + 1_000)).toBe('Grep')
152
+ expect(lookupScopedGrant(store, 'clerk', 'Grep', JSON.stringify({ pattern: 'baz', path: '/state/other.ts' }), T0 + 2_000)).toBe('Grep')
153
+ // …and it still fails closed after the window.
154
+ expect(lookupScopedGrant(store, 'clerk', 'Grep', JSON.stringify({ pattern: 'bar' }), T0 + TTL)).toBeNull()
155
+ })
156
+
124
157
  it('FIXED window — a matching call never extends expiresAt', () => {
125
158
  const store: ScopedGrantStore = new Map()
126
159
  recordScopedGrant(store, 'clerk', 'Edit(/state/x.ts)', T0, TTL)