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.
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();
|
|
@@ -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 `) +
|
|
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 ${
|
|
82582
|
-
` + ` 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
|
|
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
|
@@ -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.
|
|
54424
|
-
var COMMIT_SHA = "
|
|
54425
|
-
var COMMIT_DATE = "2026-06-
|
|
54426
|
-
var LATEST_PR =
|
|
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)
|