switchroom 0.14.32 → 0.14.34

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.
@@ -11219,6 +11219,7 @@ var profileFields = {
11219
11219
  }).optional()
11220
11220
  }).optional(),
11221
11221
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11222
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11222
11223
  reactions: ReactionsSchema,
11223
11224
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11224
11225
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11287,6 +11288,7 @@ var AgentSchema = exports_external.object({
11287
11288
  tools: AgentToolsSchema,
11288
11289
  memory: AgentMemorySchema,
11289
11290
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11291
+ 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")).optional(),
11290
11292
  reactions: ReactionsSchema,
11291
11293
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11292
11294
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11756,6 +11758,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
11756
11758
  deny: dedupe([...dDeny, ...aDeny])
11757
11759
  };
11758
11760
  }
11761
+ if (defaults.secrets || merged.secrets) {
11762
+ merged.secrets = dedupe([
11763
+ ...defaults.secrets ?? [],
11764
+ ...merged.secrets ?? []
11765
+ ]);
11766
+ }
11759
11767
  if (defaults.soul || merged.soul) {
11760
11768
  const base = defaults.soul ?? {};
11761
11769
  const override = merged.soul ?? {};
@@ -11219,6 +11219,7 @@ var profileFields = {
11219
11219
  }).optional()
11220
11220
  }).optional(),
11221
11221
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11222
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11222
11223
  reactions: ReactionsSchema,
11223
11224
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11224
11225
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11287,6 +11288,7 @@ var AgentSchema = exports_external.object({
11287
11288
  tools: AgentToolsSchema,
11288
11289
  memory: AgentMemorySchema,
11289
11290
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11291
+ 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")).optional(),
11290
11292
  reactions: ReactionsSchema,
11291
11293
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11292
11294
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11756,6 +11758,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
11756
11758
  deny: dedupe([...dDeny, ...aDeny])
11757
11759
  };
11758
11760
  }
11761
+ if (defaults.secrets || merged.secrets) {
11762
+ merged.secrets = dedupe([
11763
+ ...defaults.secrets ?? [],
11764
+ ...merged.secrets ?? []
11765
+ ]);
11766
+ }
11759
11767
  if (defaults.soul || merged.soul) {
11760
11768
  const base = defaults.soul ?? {};
11761
11769
  const override = merged.soul ?? {};
@@ -110,6 +110,138 @@ async function runAutoaccept(opts) {
110
110
  return { fired, reason: "manual-stop" };
111
111
  }
112
112
 
113
+ // src/agents/autoaccept.ts
114
+ import { execFileSync as execFileSync2 } from "node:child_process";
115
+ var PROMPTS2 = [
116
+ {
117
+ name: "dev-channels-loading",
118
+ match: /Loading.{1,30}development.{1,30}channels/,
119
+ keys: ["Enter"]
120
+ },
121
+ {
122
+ name: "dev-channels-local",
123
+ match: /using this for local development/,
124
+ keys: ["Enter"]
125
+ },
126
+ {
127
+ name: "dev-channels",
128
+ match: /I.{0,5}accept.{0,80}development.{0,10}channels/,
129
+ keys: ["Down", "Enter"]
130
+ },
131
+ {
132
+ name: "mcp-trust",
133
+ match: /Use this and all future MCP servers/,
134
+ keys: ["Enter"]
135
+ },
136
+ {
137
+ name: "theme",
138
+ match: /Choose.{1,30}text.{1,30}style/,
139
+ keys: ["Enter"]
140
+ },
141
+ {
142
+ name: "provider",
143
+ match: /Anthropic.{1,80}Bedrock/,
144
+ keys: ["Enter"]
145
+ },
146
+ {
147
+ name: "enter-to-confirm",
148
+ match: /Enter.{1,30}confirm/,
149
+ keys: ["Enter"]
150
+ }
151
+ ];
152
+ function capturePane2(agentName) {
153
+ const socket = `switchroom-${agentName}`;
154
+ try {
155
+ const out = execFileSync2("tmux", ["-L", socket, "capture-pane", "-p", "-t", agentName], {
156
+ timeout: 3000,
157
+ stdio: ["ignore", "pipe", "pipe"],
158
+ maxBuffer: 4 * 1024 * 1024
159
+ });
160
+ return out.toString("utf8");
161
+ } catch (err) {
162
+ console.error(`[autoaccept] ${agentName}: capture-pane failed: ${err.message}`);
163
+ return "";
164
+ }
165
+ }
166
+ function sendKeys2(agentName, keys) {
167
+ const socket = `switchroom-${agentName}`;
168
+ try {
169
+ execFileSync2("tmux", ["-L", socket, "send-keys", "-t", agentName, ...keys], { timeout: 3000, stdio: ["ignore", "pipe", "pipe"] });
170
+ return true;
171
+ } catch (err) {
172
+ console.error(`[autoaccept] ${agentName}: send-keys ${keys.join(" ")} failed: ${err.message}`);
173
+ return false;
174
+ }
175
+ }
176
+
177
+ // src/agents/wedge-watchdog.ts
178
+ var WEDGE_FOOTER_SIGNATURE = /(?=[\s\S]*[Ee]sc(?:ape)?[^\n]*cancel)(?=[\s\S]*(?:to select|to navigate|\u2191\/\u2193))/;
179
+ var DEFAULT_POLL_MS2 = 5000;
180
+ var DEFAULT_STABILITY_THRESHOLD = 3;
181
+ var DEFAULT_COOLDOWN_MS = 60000;
182
+ function defaultSleep2(ms) {
183
+ return new Promise((r) => setTimeout(r, ms));
184
+ }
185
+ function stabilityKey(text) {
186
+ return text.split(`
187
+ `).map((l) => l.replace(/\s+$/, "")).join(`
188
+ `);
189
+ }
190
+ async function runWedgeWatchdog(opts) {
191
+ const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS2;
192
+ const stabilityThreshold = opts.stabilityThreshold ?? DEFAULT_STABILITY_THRESHOLD;
193
+ const cooldownMs = opts.cooldownMs ?? DEFAULT_COOLDOWN_MS;
194
+ const deferToPrompts = opts.deferToPrompts ?? PROMPTS2;
195
+ const signature = opts.wedgeSignature ?? WEDGE_FOOTER_SIGNATURE;
196
+ const maxPolls = opts.maxPolls ?? Number.POSITIVE_INFINITY;
197
+ const now = opts.now ?? Date.now;
198
+ const sleep = opts.sleep ?? defaultSleep2;
199
+ const capture = opts.capture ?? capturePane2;
200
+ const send = opts.send ?? sendKeys2;
201
+ let stableCount = 0;
202
+ let lastKey = null;
203
+ let cooldownUntil = 0;
204
+ let fires = 0;
205
+ let polls = 0;
206
+ while (polls < maxPolls) {
207
+ polls++;
208
+ let text = "";
209
+ try {
210
+ text = capture(opts.agentName);
211
+ } catch (err) {
212
+ console.error(`[wedge-watchdog] ${opts.agentName}: capture threw: ${err.message}`);
213
+ text = "";
214
+ }
215
+ const isBlockingModal = !!text && signature.test(text) && !deferToPrompts.some((p) => p.match.test(text));
216
+ if (isBlockingModal) {
217
+ const key = stabilityKey(text);
218
+ if (key === lastKey) {
219
+ stableCount++;
220
+ } else {
221
+ stableCount = 1;
222
+ lastKey = key;
223
+ }
224
+ if (stableCount >= stabilityThreshold && now() >= cooldownUntil) {
225
+ console.error(`[wedge-watchdog] ${opts.agentName}: dismissing stuck blocking prompt ` + `(Esc) after ${stableCount} stable polls (~${stableCount * pollIntervalMs / 1000}s) \u2014 no human to answer it`);
226
+ try {
227
+ send(opts.agentName, ["Escape"]);
228
+ } catch (err) {
229
+ console.error(`[wedge-watchdog] ${opts.agentName}: send threw: ${err.message}`);
230
+ }
231
+ fires++;
232
+ cooldownUntil = now() + cooldownMs;
233
+ stableCount = 0;
234
+ lastKey = null;
235
+ }
236
+ } else {
237
+ stableCount = 0;
238
+ lastKey = null;
239
+ }
240
+ await sleep(pollIntervalMs);
241
+ }
242
+ return { fires, polls, reason: "max-polls" };
243
+ }
244
+
113
245
  // src/cli/autoaccept-poll.ts
114
246
  async function main() {
115
247
  const agentName = process.argv[2];
@@ -119,9 +251,20 @@ async function main() {
119
251
  }
120
252
  try {
121
253
  const res = await runAutoaccept({ agentName });
122
- console.error(`[autoaccept-poll] ${agentName}: done reason=${res.reason} fired=${res.fired.length ? res.fired.join(",") : "(none)"}`);
254
+ console.error(`[autoaccept-poll] ${agentName}: boot done reason=${res.reason} fired=${res.fired.length ? res.fired.join(",") : "(none)"}`);
255
+ } catch (err) {
256
+ console.error(`[autoaccept-poll] ${agentName}: boot unexpected throw: ${err.message}`);
257
+ }
258
+ if (process.env.SWITCHROOM_WEDGE_WATCHDOG === "0") {
259
+ console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog disabled (SWITCHROOM_WEDGE_WATCHDOG=0) \u2014 exiting after boot phase`);
260
+ process.exit(0);
261
+ }
262
+ try {
263
+ console.error(`[autoaccept-poll] ${agentName}: entering wedge-watchdog (continuous)`);
264
+ const res = await runWedgeWatchdog({ agentName });
265
+ console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog returned reason=${res.reason} fires=${res.fires}`);
123
266
  } catch (err) {
124
- console.error(`[autoaccept-poll] ${agentName}: unexpected throw: ${err.message}`);
267
+ console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog unexpected throw: ${err.message}`);
125
268
  }
126
269
  process.exit(0);
127
270
  }
@@ -11966,6 +11966,7 @@ var profileFields = {
11966
11966
  }).optional()
11967
11967
  }).optional(),
11968
11968
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11969
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11969
11970
  reactions: ReactionsSchema,
11970
11971
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11971
11972
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -12034,6 +12035,7 @@ var AgentSchema = exports_external.object({
12034
12035
  tools: AgentToolsSchema,
12035
12036
  memory: AgentMemorySchema,
12036
12037
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
12038
+ 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")).optional(),
12037
12039
  reactions: ReactionsSchema,
12038
12040
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
12039
12041
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -12505,6 +12507,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
12505
12507
  deny: dedupe([...dDeny, ...aDeny])
12506
12508
  };
12507
12509
  }
12510
+ if (defaults.secrets || merged.secrets) {
12511
+ merged.secrets = dedupe([
12512
+ ...defaults.secrets ?? [],
12513
+ ...merged.secrets ?? []
12514
+ ]);
12515
+ }
12508
12516
  if (defaults.soul || merged.soul) {
12509
12517
  const base = defaults.soul ?? {};
12510
12518
  const override = merged.soul ?? {};
@@ -13783,6 +13783,7 @@ var init_schema = __esm(() => {
13783
13783
  }).optional()
13784
13784
  }).optional(),
13785
13785
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
13786
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
13786
13787
  reactions: ReactionsSchema,
13787
13788
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
13788
13789
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -13851,6 +13852,7 @@ var init_schema = __esm(() => {
13851
13852
  tools: AgentToolsSchema,
13852
13853
  memory: AgentMemorySchema,
13853
13854
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
13855
+ 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")).optional(),
13854
13856
  reactions: ReactionsSchema,
13855
13857
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
13856
13858
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -14372,6 +14374,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14372
14374
  deny: dedupe([...dDeny, ...aDeny])
14373
14375
  };
14374
14376
  }
14377
+ if (defaults.secrets || merged.secrets) {
14378
+ merged.secrets = dedupe([
14379
+ ...defaults.secrets ?? [],
14380
+ ...merged.secrets ?? []
14381
+ ]);
14382
+ }
14375
14383
  if (defaults.soul || merged.soul) {
14376
14384
  const base = defaults.soul ?? {};
14377
14385
  const override = merged.soul ?? {};
@@ -27941,11 +27949,21 @@ function checkAclByAgent(config, agentName, key) {
27941
27949
  return { allow: true };
27942
27950
  }
27943
27951
  }
27952
+ const cfgSecrets = config;
27953
+ const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
27954
+ const standingSecrets = [
27955
+ ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
27956
+ ...Array.isArray(profileSecrets) ? profileSecrets : [],
27957
+ ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
27958
+ ];
27959
+ if (standingSecrets.includes(key)) {
27960
+ return { allow: true };
27961
+ }
27944
27962
  const schedule = agentConfig.schedule ?? [];
27945
27963
  if (schedule.length === 0) {
27946
27964
  return {
27947
27965
  allow: false,
27948
- reason: `agent '${agentName}' has no schedule entries declaring 'secrets' and no mcp_servers.*.secrets[] declaring '${key}'; nothing is broker-accessible`
27966
+ reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
27949
27967
  };
27950
27968
  }
27951
27969
  for (const entry of schedule) {
@@ -49420,8 +49438,8 @@ var {
49420
49438
  } = import__.default;
49421
49439
 
49422
49440
  // src/build-info.ts
49423
- var VERSION = "0.14.32";
49424
- var COMMIT_SHA = "d9254a62";
49441
+ var VERSION = "0.14.34";
49442
+ var COMMIT_SHA = "05829ce8";
49425
49443
 
49426
49444
  // src/cli/agent.ts
49427
49445
  init_source();
@@ -50523,6 +50541,7 @@ var DEFAULT_READ_ONLY_PREAPPROVED_TOOLS = [
50523
50541
  "Skill"
50524
50542
  ];
50525
50543
  var WEBKITE_FLEET_DENY_TOOLS = ["WebFetch", "WebSearch"];
50544
+ var INTERACTIVE_TUI_FLEET_DENY_TOOLS = ["AskUserQuestion"];
50526
50545
  var WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite";
50527
50546
  function webkiteBinaryAvailable() {
50528
50547
  const override = process.env.SWITCHROOM_WEBKITE_BINARY;
@@ -51107,7 +51126,8 @@ function buildWorkspaceContext(args) {
51107
51126
  tools,
51108
51127
  toolsDeny: dedupe2([
51109
51128
  ...tools.deny ?? [],
51110
- ...webkiteDenyForAgent(agentConfig)
51129
+ ...webkiteDenyForAgent(agentConfig),
51130
+ ...INTERACTIVE_TUI_FLEET_DENY_TOOLS
51111
51131
  ]),
51112
51132
  permissionAllow,
51113
51133
  defaultModeAcceptEdits: hasAllWildcard,
@@ -51392,6 +51412,12 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
51392
51412
  allow.push(t);
51393
51413
  }
51394
51414
  settings.permissions.allow = allow;
51415
+ const deny = Array.isArray(settings.permissions.deny) ? settings.permissions.deny : [];
51416
+ for (const t of INTERACTIVE_TUI_FLEET_DENY_TOOLS) {
51417
+ if (!deny.includes(t))
51418
+ deny.push(t);
51419
+ }
51420
+ settings.permissions.deny = deny;
51395
51421
  if (settings.mcpServers && "switchroom" in settings.mcpServers) {
51396
51422
  delete settings.mcpServers["switchroom"];
51397
51423
  }
@@ -52126,7 +52152,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
52126
52152
  ]);
52127
52153
  const desiredDeny = dedupe2([
52128
52154
  ...tools.deny ?? [],
52129
- ...webkiteDenyForAgent(agentConfig)
52155
+ ...webkiteDenyForAgent(agentConfig),
52156
+ ...INTERACTIVE_TUI_FLEET_DENY_TOOLS
52130
52157
  ]);
52131
52158
  let topicId = agentConfig.topic_id;
52132
52159
  if (topicId === undefined) {
@@ -13954,6 +13954,7 @@ var profileFields = {
13954
13954
  }).optional()
13955
13955
  }).optional(),
13956
13956
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
13957
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
13957
13958
  reactions: ReactionsSchema,
13958
13959
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
13959
13960
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -14022,6 +14023,7 @@ var AgentSchema = exports_external.object({
14022
14023
  tools: AgentToolsSchema,
14023
14024
  memory: AgentMemorySchema,
14024
14025
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
14026
+ 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")).optional(),
14025
14027
  reactions: ReactionsSchema,
14026
14028
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
14027
14029
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -14501,6 +14503,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14501
14503
  deny: dedupe([...dDeny, ...aDeny])
14502
14504
  };
14503
14505
  }
14506
+ if (defaults.secrets || merged.secrets) {
14507
+ merged.secrets = dedupe([
14508
+ ...defaults.secrets ?? [],
14509
+ ...merged.secrets ?? []
14510
+ ]);
14511
+ }
14504
14512
  if (defaults.soul || merged.soul) {
14505
14513
  const base = defaults.soul ?? {};
14506
14514
  const override = merged.soul ?? {};
@@ -14918,7 +14926,11 @@ import {
14918
14926
  writeFileSync as writeFileSync3,
14919
14927
  renameSync,
14920
14928
  mkdirSync as mkdirSync2,
14921
- unlinkSync
14929
+ openSync as openSync2,
14930
+ ftruncateSync,
14931
+ writeSync,
14932
+ fsyncSync,
14933
+ closeSync as closeSync2
14922
14934
  } from "node:fs";
14923
14935
  import { join as join3, dirname as dirname4, resolve as resolve5 } from "node:path";
14924
14936
  import { randomUUID as randomUUID2, randomBytes } from "node:crypto";
@@ -20740,10 +20752,23 @@ function formatConfigApprovalDenyError(approval, approvalId) {
20740
20752
  const suffix = approval.reason ? `: ${approval.reason}` : "";
20741
20753
  return `E_DENIED: operator denied config_propose_edit${suffix} (approval_id=${approvalId})`;
20742
20754
  }
20743
- function unlinkSyncBestEffort(path2) {
20755
+ function writeFileInPlacePreservingInode(targetPath, content) {
20756
+ const buf = Buffer.from(content, "utf-8");
20757
+ const fd = openSync2(targetPath, "r+");
20744
20758
  try {
20745
- unlinkSync(path2);
20746
- } catch {}
20759
+ ftruncateSync(fd, 0);
20760
+ let off = 0;
20761
+ while (off < buf.length) {
20762
+ off += writeSync(fd, buf, off, buf.length - off, off);
20763
+ }
20764
+ fsyncSync(fd);
20765
+ } finally {
20766
+ closeSync2(fd);
20767
+ }
20768
+ const readBack = readFileSync5(targetPath);
20769
+ if (readBack.length !== buf.length) {
20770
+ throw new Error(`in-place write short: wrote ${buf.length} bytes but read back ${readBack.length}`);
20771
+ }
20747
20772
  }
20748
20773
  var STATUS_RETENTION_MS = 10 * 60 * 1000;
20749
20774
  var STATUS_MAX_ENTRIES = 256;
@@ -21341,15 +21366,15 @@ class HostdServer {
21341
21366
  return this.reconcileFailedRolledBack(`snapshot read failed: ${e.message}`, req, caller, started);
21342
21367
  }
21343
21368
  const postApply = verdict.postApplyContent;
21344
- const tmp = configPath + ".tmp";
21345
21369
  try {
21346
- writeFileSync3(tmp, postApply);
21347
- renameSync(tmp, configPath);
21370
+ writeFileInPlacePreservingInode(configPath, postApply);
21348
21371
  } catch (e) {
21349
- unlinkSyncBestEffort(tmp);
21372
+ try {
21373
+ writeFileInPlacePreservingInode(configPath, snapshot);
21374
+ } catch {}
21350
21375
  await approval.finalize({
21351
21376
  outcome: "reconcile_failed_rolled_back",
21352
- detail: `atomic write failed: ${e.message}`
21377
+ detail: `in-place write failed: ${e.message}`
21353
21378
  });
21354
21379
  return this.reconcileFailedRolledBack(`write failed: ${e.message}`, req, caller, started);
21355
21380
  }
@@ -21369,8 +21394,7 @@ class HostdServer {
21369
21394
  }
21370
21395
  let rollbackDetail = "";
21371
21396
  try {
21372
- writeFileSync3(tmp, snapshot);
21373
- renameSync(tmp, configPath);
21397
+ writeFileInPlacePreservingInode(configPath, snapshot);
21374
21398
  } catch (e) {
21375
21399
  rollbackDetail = `snapshot restore failed: ${e.message}`;
21376
21400
  await approval.finalize({
@@ -4106,6 +4106,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
4106
4106
  deny: dedupe([...dDeny, ...aDeny])
4107
4107
  };
4108
4108
  }
4109
+ if (defaults.secrets || merged.secrets) {
4110
+ merged.secrets = dedupe([
4111
+ ...defaults.secrets ?? [],
4112
+ ...merged.secrets ?? []
4113
+ ]);
4114
+ }
4109
4115
  if (defaults.soul || merged.soul) {
4110
4116
  const base = defaults.soul ?? {};
4111
4117
  const override = merged.soul ?? {};
@@ -11534,6 +11540,7 @@ var init_schema = __esm(() => {
11534
11540
  }).optional()
11535
11541
  }).optional(),
11536
11542
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11543
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11537
11544
  reactions: ReactionsSchema,
11538
11545
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11539
11546
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11602,6 +11609,7 @@ var init_schema = __esm(() => {
11602
11609
  tools: AgentToolsSchema,
11603
11610
  memory: AgentMemorySchema,
11604
11611
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11612
+ 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")).optional(),
11605
11613
  reactions: ReactionsSchema,
11606
11614
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11607
11615
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -140,6 +140,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
140
140
  deny: dedupe([...dDeny, ...aDeny])
141
141
  };
142
142
  }
143
+ if (defaults.secrets || merged.secrets) {
144
+ merged.secrets = dedupe([
145
+ ...defaults.secrets ?? [],
146
+ ...merged.secrets ?? []
147
+ ]);
148
+ }
143
149
  if (defaults.soul || merged.soul) {
144
150
  const base = defaults.soul ?? {};
145
151
  const override = merged.soul ?? {};
@@ -11534,6 +11540,7 @@ var init_schema = __esm(() => {
11534
11540
  }).optional()
11535
11541
  }).optional(),
11536
11542
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11543
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11537
11544
  reactions: ReactionsSchema,
11538
11545
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11539
11546
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11602,6 +11609,7 @@ var init_schema = __esm(() => {
11602
11609
  tools: AgentToolsSchema,
11603
11610
  memory: AgentMemorySchema,
11604
11611
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11612
+ 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")).optional(),
11605
11613
  reactions: ReactionsSchema,
11606
11614
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11607
11615
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -13288,11 +13296,21 @@ function checkAclByAgent(config, agentName, key) {
13288
13296
  return { allow: true };
13289
13297
  }
13290
13298
  }
13299
+ const cfgSecrets = config;
13300
+ const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
13301
+ const standingSecrets = [
13302
+ ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
13303
+ ...Array.isArray(profileSecrets) ? profileSecrets : [],
13304
+ ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
13305
+ ];
13306
+ if (standingSecrets.includes(key)) {
13307
+ return { allow: true };
13308
+ }
13291
13309
  const schedule = agentConfig.schedule ?? [];
13292
13310
  if (schedule.length === 0) {
13293
13311
  return {
13294
13312
  allow: false,
13295
- reason: `agent '${agentName}' has no schedule entries declaring 'secrets' and no mcp_servers.*.secrets[] declaring '${key}'; nothing is broker-accessible`
13313
+ reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
13296
13314
  };
13297
13315
  }
13298
13316
  for (const entry of schedule) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.32",
3
+ "version": "0.14.34",
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": {
@@ -111,10 +111,15 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
111
111
  echo "[start.sh] channels.telegram.enabled=false — skipping gateway sidecar" >&2
112
112
  fi
113
113
 
114
- # 2) autoaccept-poll — first-run TUI prompt dispatcher. Single-shot
115
- # by design (exits cleanly after idle-timeout once prompts have
116
- # fired); the supervisor's exponential backoff keeps a flaky
117
- # autoaccept from busy-looping.
114
+ # 2) autoaccept-poll — first-run TUI prompt dispatcher, then continuous
115
+ # wedge-watchdog. Two phases in one process: a one-shot boot phase
116
+ # dispatches the first-run prompts and returns after idle-timeout,
117
+ # then the process stays alive running the wedge-watchdog (dismisses a
118
+ # stuck blocking modal selector mid-session — the AskUserQuestion /
119
+ # ExitPlanMode class — with Esc). So it is NORMALLY long-lived; the
120
+ # supervisor only respawns it if it crashes/exits, and its backoff
121
+ # keeps a flaky run from busy-looping. Set SWITCHROOM_WEDGE_WATCHDOG=0
122
+ # to restore the legacy boot-only single-shot behaviour.
118
123
  if [ -f /opt/switchroom/autoaccept-poll.js ] && command -v bun >/dev/null 2>&1; then
119
124
  _switchroom_supervise autoaccept /var/log/switchroom/autoaccept.log \
120
125
  bun /opt/switchroom/autoaccept-poll.js "{{name}}" &
@@ -23925,6 +23925,7 @@ var init_schema = __esm(() => {
23925
23925
  }).optional()
23926
23926
  }).optional(),
23927
23927
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
23928
+ 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")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
23928
23929
  reactions: ReactionsSchema,
23929
23930
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
23930
23931
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -23993,6 +23994,7 @@ var init_schema = __esm(() => {
23993
23994
  tools: AgentToolsSchema,
23994
23995
  memory: AgentMemorySchema,
23995
23996
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
23997
+ 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")).optional(),
23996
23998
  reactions: ReactionsSchema,
23997
23999
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
23998
24000
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -24472,6 +24474,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
24472
24474
  deny: dedupe([...dDeny, ...aDeny])
24473
24475
  };
24474
24476
  }
24477
+ if (defaults.secrets || merged.secrets) {
24478
+ merged.secrets = dedupe([
24479
+ ...defaults.secrets ?? [],
24480
+ ...merged.secrets ?? []
24481
+ ]);
24482
+ }
24475
24483
  if (defaults.soul || merged.soul) {
24476
24484
  const base = defaults.soul ?? {};
24477
24485
  const override = merged.soul ?? {};
@@ -44481,6 +44489,12 @@ function mergeAgentConfig2(defaultsIn, agentIn) {
44481
44489
  deny: dedupe2([...dDeny, ...aDeny])
44482
44490
  };
44483
44491
  }
44492
+ if (defaults.secrets || merged.secrets) {
44493
+ merged.secrets = dedupe2([
44494
+ ...defaults.secrets ?? [],
44495
+ ...merged.secrets ?? []
44496
+ ]);
44497
+ }
44484
44498
  if (defaults.soul || merged.soul) {
44485
44499
  const base = defaults.soul ?? {};
44486
44500
  const override = merged.soul ?? {};
@@ -51766,10 +51780,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
51766
51780
  }
51767
51781
 
51768
51782
  // ../src/build-info.ts
51769
- var VERSION = "0.14.32";
51770
- var COMMIT_SHA = "d9254a62";
51771
- var COMMIT_DATE = "2026-06-01T07:50:15Z";
51772
- var LATEST_PR = 2062;
51783
+ var VERSION = "0.14.34";
51784
+ var COMMIT_SHA = "05829ce8";
51785
+ var COMMIT_DATE = "2026-06-01T13:11:35Z";
51786
+ var LATEST_PR = 2068;
51773
51787
  var COMMITS_AHEAD_OF_TAG = 0;
51774
51788
 
51775
51789
  // gateway/boot-version.ts