switchroom 0.14.12 → 0.14.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.
@@ -23445,9 +23445,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23445
23445
  if (existsSync14(`${hostHomeForChecks}/.switchroom/host-control-audit.log`)) {
23446
23446
  lines.push(` - ${homePrefix}/.switchroom/host-control-audit.log:/state/agent/home/.switchroom/host-control-audit.log:ro`);
23447
23447
  }
23448
- if (hostControlEnabled && existsSync14(`${hostHomeForChecks}/.switchroom/hostd/${a.name}`)) {
23449
- lines.push(` - ${homePrefix}/.switchroom/hostd/${a.name}:/run/switchroom/hostd/${a.name}`);
23450
- }
23448
+ }
23449
+ if (hostControlEnabled && existsSync14(`${hostHomeForChecks}/.switchroom/hostd/${a.name}`)) {
23450
+ lines.push(` - ${homePrefix}/.switchroom/hostd/${a.name}:/run/switchroom/hostd/${a.name}`);
23451
23451
  }
23452
23452
  if (a.bindMounts.length > 0) {
23453
23453
  if (!a.admin) {
@@ -49413,8 +49413,8 @@ var {
49413
49413
  } = import__.default;
49414
49414
 
49415
49415
  // src/build-info.ts
49416
- var VERSION = "0.14.12";
49417
- var COMMIT_SHA = "a6cc0835";
49416
+ var VERSION = "0.14.13";
49417
+ var COMMIT_SHA = "240594e9";
49418
49418
 
49419
49419
  // src/cli/agent.ts
49420
49420
  init_source();
@@ -82066,7 +82066,7 @@ services:
82066
82066
  - no-new-privileges:true
82067
82067
  volumes:
82068
82068
  # Bind-mounts the entire ~/.switchroom dir so the daemon can:
82069
- # - create ~/.switchroom/hostd/<agent>/sock per admin agent
82069
+ # - create ~/.switchroom/hostd/<agent>/sock per agent
82070
82070
  # - append to ~/.switchroom/host-control-audit.log
82071
82071
  - ${hostHome}/.switchroom:/host-home/.switchroom:rw
82072
82072
  # ~/.switchroom/switchroom.yaml is a symlink on many operator
@@ -82143,10 +82143,10 @@ async function doInstall(opts, program3) {
82143
82143
 
82144
82144
  ` + "Continuing anyway \u2014 the install completes (image-pinned compose file written), but `docker compose up` will fail-fast."));
82145
82145
  }
82146
- const adminAgents = Object.entries(cfg.agents ?? {}).filter(([, a]) => a?.admin === true).map(([name]) => name);
82147
- if (adminAgents.length === 0) {
82148
- console.error(source_default.yellow(`No admin-flagged agents in switchroom.yaml. The daemon binds one socket per admin agent \u2014 with none, it will exit on startup.
82149
- ` + "Set `admin: true` on at least one agent (typically the test runner or a dedicated operator agent)."));
82146
+ const allAgents = Object.keys(cfg.agents ?? {});
82147
+ if (allAgents.length === 0) {
82148
+ console.error(source_default.yellow(`No agents in switchroom.yaml. The daemon binds one socket per agent \u2014 with none, it will exit on startup.
82149
+ ` + "Add at least one agent before installing hostd."));
82150
82150
  }
82151
82151
  const dir = hostdDir();
82152
82152
  const composePath = hostdComposePath();
@@ -82167,7 +82167,9 @@ async function doInstall(opts, program3) {
82167
82167
  console.log(source_default.dim(` Backed up existing compose to ${bak}`));
82168
82168
  writeFileSync39(composePath, yaml, "utf8");
82169
82169
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
82170
- console.log(source_default.dim(` admin agents: ${adminAgents.length === 0 ? "(none)" : adminAgents.join(", ")}`));
82170
+ const adminAgents = Object.entries(cfg.agents ?? {}).filter(([, a]) => a?.admin === true).map(([name]) => name);
82171
+ console.log(source_default.dim(` agents served (one socket each): ${allAgents.length === 0 ? "(none)" : allAgents.join(", ")}`));
82172
+ console.log(source_default.dim(` admin agents (full config-edit verbs): ${adminAgents.length === 0 ? "(none)" : adminAgents.join(", ")}`));
82171
82173
  console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${opts.tag ?? DEFAULT_IMAGE_TAG}\u2026`));
82172
82174
  const pull = runDocker(["compose", "-p", HOSTD_COMPOSE_PROJECT, "-f", composePath, "pull"]);
82173
82175
  if (!pull.ok) {
@@ -20192,6 +20192,7 @@ import { mkdtempSync, writeFileSync as writeFileSync2, rmSync, existsSync as exi
20192
20192
  import { tmpdir } from "node:os";
20193
20193
  import { join as join2, isAbsolute as isAbsolute2, normalize } from "node:path";
20194
20194
  import { spawnSync } from "node:child_process";
20195
+ import { isDeepStrictEqual } from "node:util";
20195
20196
  var MAX_PATCH_BYTES = 1024 * 1024;
20196
20197
  var UNLOCK_CARD_YAML_ALLOWLIST = new Set([
20197
20198
  "hostd.config_edit_enabled"
@@ -20528,6 +20529,68 @@ function validateConfigEdit(opts) {
20528
20529
  return leakErr;
20529
20530
  return { ok: true, postApplyContent: applied.after };
20530
20531
  }
20532
+ function assertSelfScopedAllowEdit(beforeContent, afterContent, caller) {
20533
+ let before;
20534
+ let after;
20535
+ try {
20536
+ before = toObject($parseDocument(beforeContent, { merge: false, strict: false }).toJS());
20537
+ after = toObject($parseDocument(afterContent, { merge: false, strict: false }).toJS());
20538
+ } catch (e) {
20539
+ return { ok: false, detail: `self-scope: config did not parse (${e.message})` };
20540
+ }
20541
+ const beforeAllow = readCallerAllow(before, caller);
20542
+ const afterAllow = readCallerAllow(after, caller);
20543
+ for (const rule of beforeAllow) {
20544
+ if (!afterAllow.includes(rule)) {
20545
+ return {
20546
+ ok: false,
20547
+ detail: `self-scope: edit removes existing allow rule "${rule}" from agents.${caller}.tools.allow`
20548
+ };
20549
+ }
20550
+ }
20551
+ const beforeStripped = stripCallerAllow(before, caller);
20552
+ const afterStripped = stripCallerAllow(after, caller);
20553
+ if (!isDeepStrictEqual(beforeStripped, afterStripped)) {
20554
+ return {
20555
+ ok: false,
20556
+ detail: `self-scope: edit changes config outside agents.${caller}.tools.allow ` + `(a non-admin agent may only widen its own allow-list)`
20557
+ };
20558
+ }
20559
+ return { ok: true };
20560
+ }
20561
+ function toObject(v) {
20562
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
20563
+ }
20564
+ function readCallerAllow(cfg, caller) {
20565
+ const agents = cfg.agents;
20566
+ if (!agents || typeof agents !== "object")
20567
+ return [];
20568
+ const agent = agents[caller];
20569
+ if (!agent || typeof agent !== "object")
20570
+ return [];
20571
+ const tools = agent.tools;
20572
+ if (!tools || typeof tools !== "object")
20573
+ return [];
20574
+ const allow = tools.allow;
20575
+ return Array.isArray(allow) ? allow.filter((x) => typeof x === "string") : [];
20576
+ }
20577
+ function stripCallerAllow(cfg, caller) {
20578
+ const clone = structuredClone(cfg);
20579
+ const agents = clone.agents;
20580
+ if (!agents || typeof agents !== "object")
20581
+ return clone;
20582
+ const agent = agents[caller];
20583
+ if (!agent || typeof agent !== "object")
20584
+ return clone;
20585
+ const tools = agent.tools;
20586
+ if (!tools || typeof tools !== "object")
20587
+ return clone;
20588
+ delete tools.allow;
20589
+ if (Object.keys(tools).length === 0) {
20590
+ delete agent.tools;
20591
+ }
20592
+ return clone;
20593
+ }
20531
20594
 
20532
20595
  // src/host-control/server.ts
20533
20596
  function resolveDigests(imageRefs) {
@@ -20877,7 +20940,7 @@ class HostdServer {
20877
20940
  case "doctor":
20878
20941
  return callerAdmin ? null : `doctor requires admin: true on caller "${caller.name}"`;
20879
20942
  case "config_propose_edit":
20880
- return callerAdmin ? null : `config_propose_edit requires admin: true on caller "${caller.name}"`;
20943
+ return null;
20881
20944
  }
20882
20945
  }
20883
20946
  async handleAgentRestart(req, caller, started) {
@@ -21138,6 +21201,18 @@ class HostdServer {
21138
21201
  if (!verdict.ok) {
21139
21202
  return err(verdict.code, verdict.detail).fixBadInput("unified_diff").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
21140
21203
  }
21204
+ if (caller.kind === "agent" && this.opts.config.agents[caller.name]?.admin !== true) {
21205
+ let beforeContent;
21206
+ try {
21207
+ beforeContent = readFileSync5(configPath, "utf-8");
21208
+ } catch {
21209
+ beforeContent = "";
21210
+ }
21211
+ const scope = assertSelfScopedAllowEdit(beforeContent, verdict.postApplyContent, caller.name);
21212
+ if (!scope.ok) {
21213
+ return err("E_NOT_SELF_SCOPED", scope.detail).why("non-admin agents may only add rules to their own " + "agents.<self>.tools.allow via config_propose_edit").fixBadInput("unified_diff").op("config_propose_edit").caller("agent").agentName(caller.name).asDenied().build(req.request_id, Date.now() - started);
21214
+ }
21215
+ }
21141
21216
  if (!this.opts.approvalGateway) {
21142
21217
  return err("E_NO_APPROVAL_GATEWAY", "validation passed but hostd was started without an approval-gateway wiring; the operator build is missing the telegram-plugin link").fixOperatorAction("infra", [
21143
21218
  "ensure hostd was launched with --approval-gateway / telegram-plugin link"
@@ -21926,13 +22001,12 @@ async function main() {
21926
22001
  process.exit(2);
21927
22002
  }
21928
22003
  const agentUids = {};
21929
- for (const [name, agent] of Object.entries(config.agents)) {
21930
- if (agent.admin === true) {
21931
- agentUids[name] = allocateAgentUid(name);
21932
- }
22004
+ for (const name of Object.keys(config.agents)) {
22005
+ agentUids[name] = allocateAgentUid(name);
21933
22006
  }
21934
22007
  if (Object.keys(agentUids).length === 0) {
21935
- process.stderr.write("hostd: no admin-flagged agents — nothing to serve. Set `admin: true` on at least one agent.\n");
22008
+ process.stderr.write(`hostd: no agents configured — nothing to serve.
22009
+ `);
21936
22010
  process.exit(2);
21937
22011
  }
21938
22012
  const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join4(homedir3(), ".switchroom", "agents");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.12",
3
+ "version": "0.14.13",
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": {
@@ -24446,6 +24446,40 @@ function createIpcClient(options) {
24446
24446
 
24447
24447
  // permission-rule.ts
24448
24448
  import { basename as basename2 } from "node:path";
24449
+ var FILE_TOOLS = new Set([
24450
+ "Edit",
24451
+ "Write",
24452
+ "MultiEdit",
24453
+ "NotebookEdit",
24454
+ "Read"
24455
+ ]);
24456
+ var BROAD_ONLY_TOOLS = new Set([
24457
+ "Glob",
24458
+ "Grep",
24459
+ "WebFetch",
24460
+ "WebSearch",
24461
+ "Task",
24462
+ "Agent",
24463
+ "TodoWrite",
24464
+ "ExitPlanMode"
24465
+ ]);
24466
+ function resolveSkillName(input) {
24467
+ return readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
24468
+ }
24469
+ function filePathFrom(input) {
24470
+ if (!input)
24471
+ return null;
24472
+ return readString(input, "file_path") ?? readString(input, "notebook_path");
24473
+ }
24474
+ function bashFirstToken(command) {
24475
+ const m = /^\s*([^\s|&;<>()`$]+)/.exec(command);
24476
+ if (!m)
24477
+ return null;
24478
+ const tok = m[1];
24479
+ if (tok.includes(".."))
24480
+ return null;
24481
+ return /^[A-Za-z0-9._\-\/]+$/.test(tok) ? tok : null;
24482
+ }
24449
24483
  function parseInput(raw) {
24450
24484
  if (!raw || typeof raw !== "string")
24451
24485
  return null;
@@ -24474,16 +24508,35 @@ function skillBasenameFromPath(input) {
24474
24508
  function matchesAllowRule(rule, toolName, inputPreview) {
24475
24509
  if (!rule || !toolName)
24476
24510
  return false;
24477
- const skillMatch = /^Skill\(([^)]+)\)$/.exec(rule);
24478
- if (skillMatch) {
24479
- if (toolName !== "Skill")
24511
+ if (rule.endsWith("__*") && rule.startsWith("mcp__")) {
24512
+ const prefix = rule.slice(0, -1);
24513
+ return toolName.startsWith(prefix);
24514
+ }
24515
+ const scoped = /^([A-Za-z]+)\((.+)\)$/.exec(rule);
24516
+ if (scoped) {
24517
+ const ruleTool = scoped[1];
24518
+ const arg = scoped[2];
24519
+ if (ruleTool !== toolName)
24480
24520
  return false;
24481
- const ruleSkill = skillMatch[1];
24482
24521
  const input = parseInput(inputPreview);
24483
- if (!input)
24484
- return false;
24485
- const reqSkill = readString(input, "skill") ?? readString(input, "skill_name") ?? readString(input, "skillName") ?? readString(input, "name") ?? skillBasenameFromPath(input);
24486
- return reqSkill === ruleSkill;
24522
+ if (ruleTool === "Skill") {
24523
+ if (!input)
24524
+ return false;
24525
+ return resolveSkillName(input) === arg;
24526
+ }
24527
+ if (ruleTool === "Bash") {
24528
+ const cmd = input ? readString(input, "command") : null;
24529
+ if (!cmd)
24530
+ return false;
24531
+ const m = /^([^:]+):\*$/.exec(arg);
24532
+ if (!m)
24533
+ return false;
24534
+ return bashFirstToken(cmd) === m[1];
24535
+ }
24536
+ if (FILE_TOOLS.has(ruleTool)) {
24537
+ return filePathFrom(input) === arg;
24538
+ }
24539
+ return false;
24487
24540
  }
24488
24541
  return rule === toolName;
24489
24542
  }