switchroom 0.15.23 → 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.
package/dist/cli/switchroom.js
CHANGED
|
@@ -50477,8 +50477,8 @@ var {
|
|
|
50477
50477
|
} = import__.default;
|
|
50478
50478
|
|
|
50479
50479
|
// src/build-info.ts
|
|
50480
|
-
var VERSION = "0.15.
|
|
50481
|
-
var COMMIT_SHA = "
|
|
50480
|
+
var VERSION = "0.15.25";
|
|
50481
|
+
var COMMIT_SHA = "0d066743";
|
|
50482
50482
|
|
|
50483
50483
|
// src/cli/agent.ts
|
|
50484
50484
|
init_source();
|
|
@@ -52078,6 +52078,21 @@ function dedupe2(items) {
|
|
|
52078
52078
|
function shellSingleQuote(s) {
|
|
52079
52079
|
return "'" + s.replace(/'/g, `'"'"'`) + "'";
|
|
52080
52080
|
}
|
|
52081
|
+
function hostHomeForBake() {
|
|
52082
|
+
return process.env.SWITCHROOM_HOST_HOME || process.env.HOME || undefined;
|
|
52083
|
+
}
|
|
52084
|
+
function hostHomeQForBake() {
|
|
52085
|
+
const h = hostHomeForBake();
|
|
52086
|
+
return h ? shellSingleQuote(h) : undefined;
|
|
52087
|
+
}
|
|
52088
|
+
function toHostHomePath(p) {
|
|
52089
|
+
const hostHome = process.env.SWITCHROOM_HOST_HOME;
|
|
52090
|
+
const containerHome = process.env.HOME;
|
|
52091
|
+
if (hostHome && containerHome && hostHome !== containerHome && (p === containerHome || p.startsWith(containerHome + "/"))) {
|
|
52092
|
+
return hostHome + p.slice(containerHome.length);
|
|
52093
|
+
}
|
|
52094
|
+
return p;
|
|
52095
|
+
}
|
|
52081
52096
|
function composeWithSidecar(renderedBase, sidecarPath) {
|
|
52082
52097
|
if (!existsSync14(sidecarPath))
|
|
52083
52098
|
return renderedBase;
|
|
@@ -52648,7 +52663,7 @@ function buildWorkspaceContext(args) {
|
|
|
52648
52663
|
} = args;
|
|
52649
52664
|
return {
|
|
52650
52665
|
name,
|
|
52651
|
-
agentDir,
|
|
52666
|
+
agentDir: toHostHomePath(agentDir),
|
|
52652
52667
|
repoRoot: REPO_ROOT,
|
|
52653
52668
|
topicId,
|
|
52654
52669
|
topicName: agentConfig.topic_name,
|
|
@@ -52687,7 +52702,7 @@ function buildWorkspaceContext(args) {
|
|
|
52687
52702
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
52688
52703
|
hindsightTopicFilterMode,
|
|
52689
52704
|
switchroomConfigPathQ: switchroomConfigPath ? shellSingleQuote(resolve11(switchroomConfigPath)) : undefined,
|
|
52690
|
-
hostHomeQ:
|
|
52705
|
+
hostHomeQ: hostHomeQForBake(),
|
|
52691
52706
|
modelQ: shellSingleQuote(resolveMainModel(agentConfig.model)),
|
|
52692
52707
|
...buildCronSessionContext(agentConfig),
|
|
52693
52708
|
thinkingEffort: agentConfig.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT,
|
|
@@ -52870,7 +52885,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
52870
52885
|
const tools = agentConfig.tools ?? { allow: [], deny: [] };
|
|
52871
52886
|
const rawAllow = tools.allow ?? [];
|
|
52872
52887
|
const hasAllWildcard = rawAllow.includes("all");
|
|
52873
|
-
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");
|
|
52874
52889
|
const dangerousMode = agentConfig.dangerous_mode === true;
|
|
52875
52890
|
const hadExplicitAllow = rawAllow.length > 0;
|
|
52876
52891
|
const readOnlyDefaults = !dangerousMode && !hadExplicitAllow ? DEFAULT_READ_ONLY_PREAPPROVED_TOOLS : [];
|
|
@@ -53704,7 +53719,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
53704
53719
|
const tools = agentConfig.tools ?? { allow: [], deny: [] };
|
|
53705
53720
|
const rawAllow = tools.allow ?? [];
|
|
53706
53721
|
const hasAllWildcard = rawAllow.includes("all");
|
|
53707
|
-
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");
|
|
53708
53723
|
const reconcileDangerousMode = agentConfig.dangerous_mode === true;
|
|
53709
53724
|
const reconcileHadExplicitAllow = rawAllow.length > 0;
|
|
53710
53725
|
const reconcileReadOnlyDefaults = !reconcileDangerousMode && !reconcileHadExplicitAllow ? DEFAULT_READ_ONLY_PREAPPROVED_TOOLS : [];
|
|
@@ -53761,7 +53776,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
53761
53776
|
const basePath = getBaseProfilePath();
|
|
53762
53777
|
const startShContext = {
|
|
53763
53778
|
name,
|
|
53764
|
-
agentDir,
|
|
53779
|
+
agentDir: toHostHomePath(agentDir),
|
|
53765
53780
|
repoRoot: REPO_ROOT,
|
|
53766
53781
|
botToken: resolvedBotToken ?? rawBotToken,
|
|
53767
53782
|
forumChatId: telegramConfig.forum_chat_id,
|
|
@@ -53778,7 +53793,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
53778
53793
|
hindsightRecallMinOverlap,
|
|
53779
53794
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
53780
53795
|
hindsightTopicFilterMode,
|
|
53781
|
-
hostHomeQ:
|
|
53796
|
+
hostHomeQ: hostHomeQForBake(),
|
|
53782
53797
|
modelQ: shellSingleQuote(resolveMainModel(agentConfig.model)),
|
|
53783
53798
|
...buildCronSessionContext(agentConfig),
|
|
53784
53799
|
thinkingEffort: agentConfig.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT,
|
|
@@ -82550,6 +82565,7 @@ Applying switchroom config...
|
|
|
82550
82565
|
process.exit(6);
|
|
82551
82566
|
}
|
|
82552
82567
|
const composePath = options.outPath ?? DEFAULT_COMPOSE_PATH2;
|
|
82568
|
+
const displayComposePath = toHostHomePath(composePath);
|
|
82553
82569
|
const operatorUid = resolveOperatorUid();
|
|
82554
82570
|
const { bytes: composeBytes } = await writeComposeFile({
|
|
82555
82571
|
config,
|
|
@@ -82560,11 +82576,11 @@ Applying switchroom config...
|
|
|
82560
82576
|
buildContext: options.buildContext
|
|
82561
82577
|
});
|
|
82562
82578
|
writeOut(source_default.bold(`
|
|
82563
|
-
Wrote `) +
|
|
82579
|
+
Wrote `) + displayComposePath + source_default.gray(` (${composeBytes} bytes)
|
|
82564
82580
|
`));
|
|
82565
82581
|
writeOut(`Bring the fleet up with:
|
|
82566
|
-
` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${
|
|
82567
|
-
` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${
|
|
82582
|
+
` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} pull && \\
|
|
82583
|
+
` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} up -d --remove-orphans
|
|
82568
82584
|
`);
|
|
82569
82585
|
writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
|
|
82570
82586
|
`));
|
package/package.json
CHANGED
|
@@ -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,11 +54424,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54420
54424
|
}
|
|
54421
54425
|
|
|
54422
54426
|
// ../src/build-info.ts
|
|
54423
|
-
var VERSION = "0.15.
|
|
54424
|
-
var COMMIT_SHA = "
|
|
54425
|
-
var COMMIT_DATE = "2026-06-
|
|
54426
|
-
var LATEST_PR =
|
|
54427
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
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;
|
|
54431
|
+
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54428
54432
|
|
|
54429
54433
|
// gateway/boot-version.ts
|
|
54430
54434
|
function formatRelativeAgo(iso) {
|
|
@@ -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)
|