switchroom 0.14.11 → 0.14.12

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.
@@ -11406,6 +11406,9 @@ var HostControlConfigSchema = exports_external.object({
11406
11406
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
11407
11407
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
11408
11408
  });
11409
+ var WebServiceConfigSchema = exports_external.object({
11410
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
11411
+ });
11409
11412
  var HostdConfigSchema = exports_external.object({
11410
11413
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
11411
11414
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -11439,6 +11442,7 @@ var SwitchroomConfigSchema = exports_external.object({
11439
11442
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11440
11443
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11441
11444
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
11445
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
11442
11446
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11443
11447
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11444
11448
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -11406,6 +11406,9 @@ var HostControlConfigSchema = exports_external.object({
11406
11406
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
11407
11407
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
11408
11408
  });
11409
+ var WebServiceConfigSchema = exports_external.object({
11410
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
11411
+ });
11409
11412
  var HostdConfigSchema = exports_external.object({
11410
11413
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
11411
11414
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -11439,6 +11442,7 @@ var SwitchroomConfigSchema = exports_external.object({
11439
11442
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11440
11443
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11441
11444
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
11445
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
11442
11446
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11443
11447
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11444
11448
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -12153,6 +12153,9 @@ var HostControlConfigSchema = exports_external.object({
12153
12153
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
12154
12154
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
12155
12155
  });
12156
+ var WebServiceConfigSchema = exports_external.object({
12157
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false \u2014 existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
12158
+ });
12156
12159
  var HostdConfigSchema = exports_external.object({
12157
12160
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
12158
12161
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit \u00a75). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -12186,6 +12189,7 @@ var SwitchroomConfigSchema = exports_external.object({
12186
12189
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
12187
12190
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
12188
12191
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a \u2014 disabled by default)."),
12192
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
12189
12193
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
12190
12194
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
12191
12195
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
13520
13520
  });
13521
13521
 
13522
13522
  // src/config/schema.ts
13523
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
13523
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
13524
13524
  var init_schema = __esm(() => {
13525
13525
  init_zod();
13526
13526
  CodeRepoEntrySchema = exports_external.object({
@@ -13970,6 +13970,9 @@ var init_schema = __esm(() => {
13970
13970
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
13971
13971
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
13972
13972
  });
13973
+ WebServiceConfigSchema = exports_external.object({
13974
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false \u2014 existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
13975
+ });
13973
13976
  HostdConfigSchema = exports_external.object({
13974
13977
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
13975
13978
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit \u00a75). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -14003,6 +14006,7 @@ var init_schema = __esm(() => {
14003
14006
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
14004
14007
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
14005
14008
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a \u2014 disabled by default)."),
14009
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
14006
14010
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
14007
14011
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
14008
14012
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -49409,8 +49413,8 @@ var {
49409
49413
  } = import__.default;
49410
49414
 
49411
49415
  // src/build-info.ts
49412
- var VERSION = "0.14.11";
49413
- var COMMIT_SHA = "89d93911";
49416
+ var VERSION = "0.14.12";
49417
+ var COMMIT_SHA = "a6cc0835";
49414
49418
 
49415
49419
  // src/cli/agent.ts
49416
49420
  init_source();
@@ -73207,6 +73211,26 @@ function planUpdate(opts) {
73207
73211
  throw new Error("switchroom hostd install failed");
73208
73212
  }
73209
73213
  });
73214
+ let webServiceManaged;
73215
+ if (typeof opts.webServiceManaged === "boolean") {
73216
+ webServiceManaged = opts.webServiceManaged;
73217
+ } else {
73218
+ try {
73219
+ webServiceManaged = loadConfig().web_service?.managed === true;
73220
+ } catch {
73221
+ webServiceManaged = false;
73222
+ }
73223
+ }
73224
+ steps.push({
73225
+ name: "refresh-web",
73226
+ description: "switchroom webd install \u2014 pull latest web-service image + recreate the dashboard/webhook container (separate compose project)",
73227
+ skipReason: !webServiceManaged ? "web_service.managed is not true \u2014 web container not in use (legacy systemd unit)" : opts.skipImages ? "--skip-images flag set" : undefined,
73228
+ run: () => {
73229
+ const r = runner(process.execPath, [scriptPath, "webd", "install"]);
73230
+ if (r.status !== 0)
73231
+ throw new Error("switchroom webd install failed");
73232
+ }
73233
+ });
73210
73234
  steps.push({
73211
73235
  name: "sync-bundled-skills",
73212
73236
  description: "Sync shipped skills/ to ~/.switchroom/skills/_bundled/ (host-stable pool dir).",
@@ -82285,6 +82309,222 @@ The log is created when hostd handles its first privileged-verb request.`));
82285
82309
  });
82286
82310
  }
82287
82311
 
82312
+ // src/cli/webd.ts
82313
+ init_source();
82314
+ init_helpers();
82315
+ import { existsSync as existsSync84, mkdirSync as mkdirSync47, writeFileSync as writeFileSync40, copyFileSync as copyFileSync13 } from "node:fs";
82316
+ import { homedir as homedir47 } from "node:os";
82317
+ import { join as join80 } from "node:path";
82318
+ import { spawnSync as spawnSync14 } from "node:child_process";
82319
+ var DEFAULT_IMAGE_TAG2 = "latest";
82320
+ var WEB_COMPOSE_PROJECT = "switchroom-web";
82321
+ function renderWebComposeFile(opts) {
82322
+ const { hostHome, imageTag, operatorUid } = opts;
82323
+ return `# AUTO-GENERATED by \`switchroom webd install\` \u2014 do not hand-edit.
82324
+ # Edits land at \`switchroom webd install\` time; backed up to
82325
+ # docker-compose.yml.bak-<ts> on overwrite.
82326
+ #
82327
+ # Separate compose project (\`switchroom-web\`) from the agent fleet
82328
+ # (\`switchroom\`) so \`compose up -d --remove-orphans\` against the fleet
82329
+ # cannot recreate this container mid-request. Same isolation contract
82330
+ # as switchroom-hostd.
82331
+
82332
+ services:
82333
+ web:
82334
+ image: ghcr.io/switchroom/switchroom-web:${imageTag}
82335
+ container_name: switchroom-web
82336
+ restart: unless-stopped
82337
+ # The server MUST own host loopback 127.0.0.1:8080 \u2014 the cloudflared
82338
+ # tunnel (GitHub webhooks) and \`tailscale serve\` (dashboard) both
82339
+ # reach it there, and the dashboard CSRF gate trusts the
82340
+ # Tailscale-User-Login header only when the request arrives on
82341
+ # loopback (PR #1380). Host networking makes the container's
82342
+ # 127.0.0.1 the host loopback, so both keep working unchanged.
82343
+ network_mode: host
82344
+ # The webhook.sock forward is peercred-gated by each agent's gateway
82345
+ # to {agent uid, SWITCHROOM_WEBHOOK_RECEIVER_UID = operator uid}.
82346
+ # Run as any other uid \u2192 every forward is 503'd. Operator uid also
82347
+ # owns ~/.switchroom so config/secret reads work.
82348
+ user: "${operatorUid}:${operatorUid}"
82349
+ # tini (image ENTRYPOINT) forwards SIGTERM to the bun process on
82350
+ # \`docker stop\`; Bun.serve shuts its listener within the grace
82351
+ # window. 15s mirrors hostd.
82352
+ stop_grace_period: 15s
82353
+ cap_drop:
82354
+ - ALL
82355
+ # Deliberately NO cap_add \u2014 the web server never chowns sockets or
82356
+ # shells out to docker. Minimal surface for the one internet-facing
82357
+ # component.
82358
+ security_opt:
82359
+ - no-new-privileges:true
82360
+ volumes:
82361
+ # The receiver reads operator-owned files under ~/.switchroom:
82362
+ # - webhook-secrets.json (per-source HMAC secrets)
82363
+ # - webhook-edge-secret (Cloudflare edge lock, Phase 2)
82364
+ # - web-token (dashboard auth)
82365
+ # and forwards verified events to per-agent webhook.sock at
82366
+ # ~/.switchroom/agents/<agent>/telegram/webhook.sock
82367
+ # so it needs the whole tree read-write (the sock connect is a
82368
+ # write-side operation on the agent dir).
82369
+ - ${hostHome}/.switchroom:/host-home/.switchroom:rw
82370
+ # ~/.switchroom/switchroom.yaml is a symlink on many operator
82371
+ # setups (into a separate config repo). Mounting the file directly
82372
+ # forces docker to follow the symlink at mount time and bind the
82373
+ # underlying file. The dashboard only READS the config, so :ro.
82374
+ # Mirrors how the agent + hostd containers expose the config at
82375
+ # /state/config/switchroom.yaml.
82376
+ - ${hostHome}/.switchroom/switchroom.yaml:/state/config/switchroom.yaml:ro
82377
+ # No docker socket is mounted \u2014 the web server never touches docker.
82378
+ environment:
82379
+ # The CLI resolves homedir() for ~/.switchroom paths; pin it to
82380
+ # /host-home (bind-mounted to the operator's home) so the paths
82381
+ # the server reads/writes match the host paths the agent fleet
82382
+ # binds (~/.switchroom/agents/<agent>/telegram/webhook.sock, \u2026).
82383
+ HOME: /host-home
82384
+ # Read the same config the agent fleet's compose generator did.
82385
+ SWITCHROOM_CONFIG: /state/config/switchroom.yaml
82386
+ # PATH must include /usr/local/bin for the switchroom shim.
82387
+ PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
82388
+ # network_mode: host means no custom network is attached; the
82389
+ # container shares the host network namespace directly.
82390
+
82391
+ # Healthcheck deferred \u2014 the server has no /healthz endpoint yet and a
82392
+ # probe that hits the dashboard route would need the web-token. For now,
82393
+ # \`switchroom webd status\` is the operator surface and the server's
82394
+ # stderr lands in \`docker logs switchroom-web\`.
82395
+ `;
82396
+ }
82397
+ function webdDir() {
82398
+ return join80(homedir47(), ".switchroom", "web");
82399
+ }
82400
+ function webdComposePath() {
82401
+ return join80(webdDir(), "docker-compose.yml");
82402
+ }
82403
+ function backupExistingCompose2() {
82404
+ const p = webdComposePath();
82405
+ if (!existsSync84(p))
82406
+ return null;
82407
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
82408
+ const bak = `${p}.bak-${ts}`;
82409
+ copyFileSync13(p, bak);
82410
+ return bak;
82411
+ }
82412
+ function runDocker2(args) {
82413
+ const r = spawnSync14("docker", args, { encoding: "utf8" });
82414
+ return {
82415
+ ok: r.status === 0,
82416
+ stdout: r.stdout ?? "",
82417
+ stderr: r.stderr ?? ""
82418
+ };
82419
+ }
82420
+ async function doInstall2(opts) {
82421
+ const operatorUid = resolveOperatorUid();
82422
+ if (operatorUid === undefined) {
82423
+ console.error(source_default.red(`Could not resolve the operator uid (no SUDO_UID and getuid() is 0 or unavailable).
82424
+ ` + `The web container must run as the operator uid so its webhook forwards pass
82425
+ ` + "each agent gateway's peercred ACL. Run `switchroom webd install` as the\n" + "operator user (not root), or under `sudo` from the operator's shell."));
82426
+ process.exit(1);
82427
+ }
82428
+ const dir = webdDir();
82429
+ const composePath = webdComposePath();
82430
+ mkdirSync47(dir, { recursive: true });
82431
+ const yaml = renderWebComposeFile({
82432
+ hostHome: homedir47(),
82433
+ imageTag: opts.tag ?? DEFAULT_IMAGE_TAG2,
82434
+ operatorUid
82435
+ });
82436
+ if (opts.dryRun) {
82437
+ console.log(source_default.dim(`# Would write: ${composePath}`));
82438
+ console.log(yaml);
82439
+ console.log(source_default.dim(`# Would run: docker compose -p ${WEB_COMPOSE_PROJECT} -f ${composePath} up -d`));
82440
+ return;
82441
+ }
82442
+ const bak = backupExistingCompose2();
82443
+ if (bak)
82444
+ console.log(source_default.dim(` Backed up existing compose to ${bak}`));
82445
+ writeFileSync40(composePath, yaml, "utf8");
82446
+ console.log(source_default.green(` \u2713 Wrote ${composePath}`));
82447
+ console.log(source_default.dim(` running as uid ${operatorUid} (operator), network_mode: host`));
82448
+ console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${opts.tag ?? DEFAULT_IMAGE_TAG2}\u2026`));
82449
+ const pull = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composePath, "pull"]);
82450
+ if (!pull.ok) {
82451
+ console.error(source_default.red(` pull failed:
82452
+ ${pull.stderr}`));
82453
+ console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${opts.tag ?? DEFAULT_IMAGE_TAG2}\` may not be published yet.
82454
+ ` + ` Check the docker-images workflow run and verify the tag at:
82455
+ ` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-web`));
82456
+ process.exit(1);
82457
+ }
82458
+ console.log(source_default.dim(` Bringing up web service\u2026`));
82459
+ const up = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composePath, "up", "-d"]);
82460
+ if (!up.ok) {
82461
+ console.error(source_default.red(` up failed:
82462
+ ${up.stderr}`));
82463
+ process.exit(1);
82464
+ }
82465
+ console.log(source_default.green(` \u2713 Web service running (project: ${WEB_COMPOSE_PROJECT})`));
82466
+ console.log(source_default.dim(` Logs: docker logs switchroom-web --tail 50`));
82467
+ console.log(source_default.dim(` Verify: switchroom webd status`));
82468
+ console.log(source_default.yellow(` NOTE: the switchroom-web.service systemd unit (if still installed) also
82469
+ ` + ` binds 127.0.0.1:8080. Stop it before this container can claim the port:
82470
+ ` + ` systemctl --user stop switchroom-web.service
82471
+ ` + ` systemctl --user disable switchroom-web.service`));
82472
+ }
82473
+ function doStatus2() {
82474
+ const composeYml = webdComposePath();
82475
+ console.log(source_default.bold("switchroom-web"));
82476
+ console.log("");
82477
+ if (!existsSync84(composeYml)) {
82478
+ console.log(source_default.yellow(" compose: not installed"));
82479
+ console.log(source_default.dim(" run `switchroom webd install` to set up."));
82480
+ return;
82481
+ }
82482
+ console.log(source_default.green(` compose: ${composeYml}`));
82483
+ const ps = runDocker2([
82484
+ "compose",
82485
+ "-p",
82486
+ WEB_COMPOSE_PROJECT,
82487
+ "-f",
82488
+ composeYml,
82489
+ "ps",
82490
+ "--format",
82491
+ "{{.Name}} {{.Status}} {{.Image}}"
82492
+ ]);
82493
+ if (!ps.ok || !ps.stdout.trim()) {
82494
+ console.log(source_default.yellow(" container: not running"));
82495
+ } else {
82496
+ console.log(source_default.green(` container: ${ps.stdout.trim()}`));
82497
+ }
82498
+ }
82499
+ function doUninstall2() {
82500
+ const composeYml = webdComposePath();
82501
+ if (!existsSync84(composeYml)) {
82502
+ console.log(source_default.yellow(" No web-service install detected (no compose file at this path)."));
82503
+ return;
82504
+ }
82505
+ console.log(source_default.dim(` Stopping ${WEB_COMPOSE_PROJECT}\u2026`));
82506
+ const down = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composeYml, "down"]);
82507
+ if (!down.ok) {
82508
+ console.error(source_default.red(` down failed:
82509
+ ${down.stderr}`));
82510
+ process.exit(1);
82511
+ }
82512
+ console.log(source_default.green(" \u2713 Web service stopped"));
82513
+ console.log(source_default.dim(` Compose file left in place at ${composeYml}`));
82514
+ console.log(source_default.dim(` To re-enable: switchroom webd install`));
82515
+ console.log(source_default.yellow(` If you cut over from the systemd unit, re-enable it to restore the
82516
+ ` + ` dashboard + webhook receiver:
82517
+ ` + ` systemctl --user enable --now switchroom-web.service`));
82518
+ }
82519
+ function registerWebdCommand(program3) {
82520
+ const webd = program3.command("webd").description("Manage switchroom-web, the dashboard + GitHub-webhook receiver container");
82521
+ webd.command("install").description("Install or refresh the web container (writes ~/.switchroom/web/docker-compose.yml + docker compose up -d)").option("--tag <tag>", "Image tag (default: latest)", DEFAULT_IMAGE_TAG2).option("--dry-run", "Print the compose file and the docker commands without writing or running anything").action(withConfigError(async (opts) => {
82522
+ await doInstall2(opts);
82523
+ }));
82524
+ webd.command("status").description("Show web-service container state").action(() => doStatus2());
82525
+ webd.command("uninstall").description("Stop the web container. Leaves the compose file in place for re-install.").action(() => doUninstall2());
82526
+ }
82527
+
82288
82528
  // src/cli/index.ts
82289
82529
  installGlobalErrorHandlers();
82290
82530
  var program3 = new Command().name("switchroom").description("Multi-agent orchestrator for Claude Code. One Telegram group, many specialized agents.").version(VERSION).option("-c, --config <path>", "Path to switchroom.yaml config file").hook("preAction", async (_thisCommand, actionCommand) => {
@@ -82333,6 +82573,7 @@ registerSkillSearchCommand(program3);
82333
82573
  registerAgentConfigMcpCommand(program3);
82334
82574
  registerHostdMcpCommand(program3);
82335
82575
  registerHostdCommand(program3);
82576
+ registerWebdCommand(program3);
82336
82577
 
82337
82578
  // bin/switchroom.ts
82338
82579
  program3.parse();
@@ -14141,6 +14141,9 @@ var HostControlConfigSchema = exports_external.object({
14141
14141
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
14142
14142
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
14143
14143
  });
14144
+ var WebServiceConfigSchema = exports_external.object({
14145
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
14146
+ });
14144
14147
  var HostdConfigSchema = exports_external.object({
14145
14148
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
14146
14149
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -14174,6 +14177,7 @@ var SwitchroomConfigSchema = exports_external.object({
14174
14177
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
14175
14178
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
14176
14179
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
14180
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
14177
14181
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
14178
14182
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
14179
14183
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -11271,7 +11271,7 @@ var init_dist = __esm(() => {
11271
11271
  });
11272
11272
 
11273
11273
  // src/config/schema.ts
11274
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11274
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11275
11275
  var init_schema = __esm(() => {
11276
11276
  init_zod();
11277
11277
  CodeRepoEntrySchema = exports_external.object({
@@ -11721,6 +11721,9 @@ var init_schema = __esm(() => {
11721
11721
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
11722
11722
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
11723
11723
  });
11724
+ WebServiceConfigSchema = exports_external.object({
11725
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
11726
+ });
11724
11727
  HostdConfigSchema = exports_external.object({
11725
11728
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
11726
11729
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -11754,6 +11757,7 @@ var init_schema = __esm(() => {
11754
11757
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11755
11758
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11756
11759
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
11760
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
11757
11761
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11758
11762
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11759
11763
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -11271,7 +11271,7 @@ var init_zod = __esm(() => {
11271
11271
  });
11272
11272
 
11273
11273
  // src/config/schema.ts
11274
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11274
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11275
11275
  var init_schema = __esm(() => {
11276
11276
  init_zod();
11277
11277
  CodeRepoEntrySchema = exports_external.object({
@@ -11721,6 +11721,9 @@ var init_schema = __esm(() => {
11721
11721
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
11722
11722
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
11723
11723
  });
11724
+ WebServiceConfigSchema = exports_external.object({
11725
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
11726
+ });
11724
11727
  HostdConfigSchema = exports_external.object({
11725
11728
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
11726
11729
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -11754,6 +11757,7 @@ var init_schema = __esm(() => {
11754
11757
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11755
11758
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11756
11759
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
11760
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
11757
11761
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11758
11762
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11759
11763
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.11",
3
+ "version": "0.14.12",
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": {
@@ -23607,7 +23607,7 @@ var init_dist = __esm(() => {
23607
23607
  });
23608
23608
 
23609
23609
  // ../src/config/schema.ts
23610
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
23610
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
23611
23611
  var init_schema = __esm(() => {
23612
23612
  init_zod();
23613
23613
  CodeRepoEntrySchema = exports_external.object({
@@ -24057,6 +24057,9 @@ var init_schema = __esm(() => {
24057
24057
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
24058
24058
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
24059
24059
  });
24060
+ WebServiceConfigSchema = exports_external.object({
24061
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false \u2014 existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
24062
+ });
24060
24063
  HostdConfigSchema = exports_external.object({
24061
24064
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
24062
24065
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit \u00a75). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
@@ -24090,6 +24093,7 @@ var init_schema = __esm(() => {
24090
24093
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
24091
24094
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
24092
24095
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a \u2014 disabled by default)."),
24096
+ web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
24093
24097
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
24094
24098
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
24095
24099
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -50788,11 +50792,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
50788
50792
  }
50789
50793
 
50790
50794
  // ../src/build-info.ts
50791
- var VERSION = "0.14.11";
50792
- var COMMIT_SHA = "89d93911";
50793
- var COMMIT_DATE = "2026-05-29T09:38:43Z";
50794
- var LATEST_PR = 1991;
50795
- var COMMITS_AHEAD_OF_TAG = 0;
50795
+ var VERSION = "0.14.12";
50796
+ var COMMIT_SHA = "a6cc0835";
50797
+ var COMMIT_DATE = "2026-05-29T20:52:05+10:00";
50798
+ var LATEST_PR = null;
50799
+ var COMMITS_AHEAD_OF_TAG = 2;
50796
50800
 
50797
50801
  // gateway/boot-version.ts
50798
50802
  function formatRelativeAgo(iso) {