switchroom 0.14.11 → 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.
- package/dist/agent-scheduler/index.js +4 -0
- package/dist/auth-broker/index.js +4 -0
- package/dist/cli/notion-write-pretool.mjs +4 -0
- package/dist/cli/switchroom.js +255 -12
- package/dist/host-control/main.js +84 -6
- package/dist/vault/approvals/kernel-server.js +5 -1
- package/dist/vault/broker/server.js +5 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +61 -8
- package/telegram-plugin/dist/gateway/gateway.js +287 -161
- package/telegram-plugin/dist/server.js +64 -9
- package/telegram-plugin/gateway/gateway.ts +78 -66
- package/telegram-plugin/gateway/ipc-protocol.ts +4 -2
- package/telegram-plugin/permission-rule.ts +200 -122
- package/telegram-plugin/permission-title.ts +209 -197
- package/telegram-plugin/tests/always-allow-grant.test.ts +86 -54
- package/telegram-plugin/tests/always-allow-persist.test.ts +35 -34
- package/telegram-plugin/tests/permission-rule.test.ts +185 -127
- package/telegram-plugin/tests/permission-title.test.ts +109 -195
|
@@ -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({
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -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({
|
|
@@ -23441,9 +23445,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23441
23445
|
if (existsSync14(`${hostHomeForChecks}/.switchroom/host-control-audit.log`)) {
|
|
23442
23446
|
lines.push(` - ${homePrefix}/.switchroom/host-control-audit.log:/state/agent/home/.switchroom/host-control-audit.log:ro`);
|
|
23443
23447
|
}
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
}
|
|
23448
|
+
}
|
|
23449
|
+
if (hostControlEnabled && existsSync14(`${hostHomeForChecks}/.switchroom/hostd/${a.name}`)) {
|
|
23450
|
+
lines.push(` - ${homePrefix}/.switchroom/hostd/${a.name}:/run/switchroom/hostd/${a.name}`);
|
|
23447
23451
|
}
|
|
23448
23452
|
if (a.bindMounts.length > 0) {
|
|
23449
23453
|
if (!a.admin) {
|
|
@@ -49409,8 +49413,8 @@ var {
|
|
|
49409
49413
|
} = import__.default;
|
|
49410
49414
|
|
|
49411
49415
|
// src/build-info.ts
|
|
49412
|
-
var VERSION = "0.14.
|
|
49413
|
-
var COMMIT_SHA = "
|
|
49416
|
+
var VERSION = "0.14.13";
|
|
49417
|
+
var COMMIT_SHA = "240594e9";
|
|
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).",
|
|
@@ -82042,7 +82066,7 @@ services:
|
|
|
82042
82066
|
- no-new-privileges:true
|
|
82043
82067
|
volumes:
|
|
82044
82068
|
# Bind-mounts the entire ~/.switchroom dir so the daemon can:
|
|
82045
|
-
# - create ~/.switchroom/hostd/<agent>/sock per
|
|
82069
|
+
# - create ~/.switchroom/hostd/<agent>/sock per agent
|
|
82046
82070
|
# - append to ~/.switchroom/host-control-audit.log
|
|
82047
82071
|
- ${hostHome}/.switchroom:/host-home/.switchroom:rw
|
|
82048
82072
|
# ~/.switchroom/switchroom.yaml is a symlink on many operator
|
|
@@ -82119,10 +82143,10 @@ async function doInstall(opts, program3) {
|
|
|
82119
82143
|
|
|
82120
82144
|
` + "Continuing anyway \u2014 the install completes (image-pinned compose file written), but `docker compose up` will fail-fast."));
|
|
82121
82145
|
}
|
|
82122
|
-
const
|
|
82123
|
-
if (
|
|
82124
|
-
console.error(source_default.yellow(`No
|
|
82125
|
-
` + "
|
|
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."));
|
|
82126
82150
|
}
|
|
82127
82151
|
const dir = hostdDir();
|
|
82128
82152
|
const composePath = hostdComposePath();
|
|
@@ -82143,7 +82167,9 @@ async function doInstall(opts, program3) {
|
|
|
82143
82167
|
console.log(source_default.dim(` Backed up existing compose to ${bak}`));
|
|
82144
82168
|
writeFileSync39(composePath, yaml, "utf8");
|
|
82145
82169
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|
|
82146
|
-
|
|
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(", ")}`));
|
|
82147
82173
|
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${opts.tag ?? DEFAULT_IMAGE_TAG}\u2026`));
|
|
82148
82174
|
const pull = runDocker(["compose", "-p", HOSTD_COMPOSE_PROJECT, "-f", composePath, "pull"]);
|
|
82149
82175
|
if (!pull.ok) {
|
|
@@ -82285,6 +82311,222 @@ The log is created when hostd handles its first privileged-verb request.`));
|
|
|
82285
82311
|
});
|
|
82286
82312
|
}
|
|
82287
82313
|
|
|
82314
|
+
// src/cli/webd.ts
|
|
82315
|
+
init_source();
|
|
82316
|
+
init_helpers();
|
|
82317
|
+
import { existsSync as existsSync84, mkdirSync as mkdirSync47, writeFileSync as writeFileSync40, copyFileSync as copyFileSync13 } from "node:fs";
|
|
82318
|
+
import { homedir as homedir47 } from "node:os";
|
|
82319
|
+
import { join as join80 } from "node:path";
|
|
82320
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
82321
|
+
var DEFAULT_IMAGE_TAG2 = "latest";
|
|
82322
|
+
var WEB_COMPOSE_PROJECT = "switchroom-web";
|
|
82323
|
+
function renderWebComposeFile(opts) {
|
|
82324
|
+
const { hostHome, imageTag, operatorUid } = opts;
|
|
82325
|
+
return `# AUTO-GENERATED by \`switchroom webd install\` \u2014 do not hand-edit.
|
|
82326
|
+
# Edits land at \`switchroom webd install\` time; backed up to
|
|
82327
|
+
# docker-compose.yml.bak-<ts> on overwrite.
|
|
82328
|
+
#
|
|
82329
|
+
# Separate compose project (\`switchroom-web\`) from the agent fleet
|
|
82330
|
+
# (\`switchroom\`) so \`compose up -d --remove-orphans\` against the fleet
|
|
82331
|
+
# cannot recreate this container mid-request. Same isolation contract
|
|
82332
|
+
# as switchroom-hostd.
|
|
82333
|
+
|
|
82334
|
+
services:
|
|
82335
|
+
web:
|
|
82336
|
+
image: ghcr.io/switchroom/switchroom-web:${imageTag}
|
|
82337
|
+
container_name: switchroom-web
|
|
82338
|
+
restart: unless-stopped
|
|
82339
|
+
# The server MUST own host loopback 127.0.0.1:8080 \u2014 the cloudflared
|
|
82340
|
+
# tunnel (GitHub webhooks) and \`tailscale serve\` (dashboard) both
|
|
82341
|
+
# reach it there, and the dashboard CSRF gate trusts the
|
|
82342
|
+
# Tailscale-User-Login header only when the request arrives on
|
|
82343
|
+
# loopback (PR #1380). Host networking makes the container's
|
|
82344
|
+
# 127.0.0.1 the host loopback, so both keep working unchanged.
|
|
82345
|
+
network_mode: host
|
|
82346
|
+
# The webhook.sock forward is peercred-gated by each agent's gateway
|
|
82347
|
+
# to {agent uid, SWITCHROOM_WEBHOOK_RECEIVER_UID = operator uid}.
|
|
82348
|
+
# Run as any other uid \u2192 every forward is 503'd. Operator uid also
|
|
82349
|
+
# owns ~/.switchroom so config/secret reads work.
|
|
82350
|
+
user: "${operatorUid}:${operatorUid}"
|
|
82351
|
+
# tini (image ENTRYPOINT) forwards SIGTERM to the bun process on
|
|
82352
|
+
# \`docker stop\`; Bun.serve shuts its listener within the grace
|
|
82353
|
+
# window. 15s mirrors hostd.
|
|
82354
|
+
stop_grace_period: 15s
|
|
82355
|
+
cap_drop:
|
|
82356
|
+
- ALL
|
|
82357
|
+
# Deliberately NO cap_add \u2014 the web server never chowns sockets or
|
|
82358
|
+
# shells out to docker. Minimal surface for the one internet-facing
|
|
82359
|
+
# component.
|
|
82360
|
+
security_opt:
|
|
82361
|
+
- no-new-privileges:true
|
|
82362
|
+
volumes:
|
|
82363
|
+
# The receiver reads operator-owned files under ~/.switchroom:
|
|
82364
|
+
# - webhook-secrets.json (per-source HMAC secrets)
|
|
82365
|
+
# - webhook-edge-secret (Cloudflare edge lock, Phase 2)
|
|
82366
|
+
# - web-token (dashboard auth)
|
|
82367
|
+
# and forwards verified events to per-agent webhook.sock at
|
|
82368
|
+
# ~/.switchroom/agents/<agent>/telegram/webhook.sock
|
|
82369
|
+
# so it needs the whole tree read-write (the sock connect is a
|
|
82370
|
+
# write-side operation on the agent dir).
|
|
82371
|
+
- ${hostHome}/.switchroom:/host-home/.switchroom:rw
|
|
82372
|
+
# ~/.switchroom/switchroom.yaml is a symlink on many operator
|
|
82373
|
+
# setups (into a separate config repo). Mounting the file directly
|
|
82374
|
+
# forces docker to follow the symlink at mount time and bind the
|
|
82375
|
+
# underlying file. The dashboard only READS the config, so :ro.
|
|
82376
|
+
# Mirrors how the agent + hostd containers expose the config at
|
|
82377
|
+
# /state/config/switchroom.yaml.
|
|
82378
|
+
- ${hostHome}/.switchroom/switchroom.yaml:/state/config/switchroom.yaml:ro
|
|
82379
|
+
# No docker socket is mounted \u2014 the web server never touches docker.
|
|
82380
|
+
environment:
|
|
82381
|
+
# The CLI resolves homedir() for ~/.switchroom paths; pin it to
|
|
82382
|
+
# /host-home (bind-mounted to the operator's home) so the paths
|
|
82383
|
+
# the server reads/writes match the host paths the agent fleet
|
|
82384
|
+
# binds (~/.switchroom/agents/<agent>/telegram/webhook.sock, \u2026).
|
|
82385
|
+
HOME: /host-home
|
|
82386
|
+
# Read the same config the agent fleet's compose generator did.
|
|
82387
|
+
SWITCHROOM_CONFIG: /state/config/switchroom.yaml
|
|
82388
|
+
# PATH must include /usr/local/bin for the switchroom shim.
|
|
82389
|
+
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
82390
|
+
# network_mode: host means no custom network is attached; the
|
|
82391
|
+
# container shares the host network namespace directly.
|
|
82392
|
+
|
|
82393
|
+
# Healthcheck deferred \u2014 the server has no /healthz endpoint yet and a
|
|
82394
|
+
# probe that hits the dashboard route would need the web-token. For now,
|
|
82395
|
+
# \`switchroom webd status\` is the operator surface and the server's
|
|
82396
|
+
# stderr lands in \`docker logs switchroom-web\`.
|
|
82397
|
+
`;
|
|
82398
|
+
}
|
|
82399
|
+
function webdDir() {
|
|
82400
|
+
return join80(homedir47(), ".switchroom", "web");
|
|
82401
|
+
}
|
|
82402
|
+
function webdComposePath() {
|
|
82403
|
+
return join80(webdDir(), "docker-compose.yml");
|
|
82404
|
+
}
|
|
82405
|
+
function backupExistingCompose2() {
|
|
82406
|
+
const p = webdComposePath();
|
|
82407
|
+
if (!existsSync84(p))
|
|
82408
|
+
return null;
|
|
82409
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
82410
|
+
const bak = `${p}.bak-${ts}`;
|
|
82411
|
+
copyFileSync13(p, bak);
|
|
82412
|
+
return bak;
|
|
82413
|
+
}
|
|
82414
|
+
function runDocker2(args) {
|
|
82415
|
+
const r = spawnSync14("docker", args, { encoding: "utf8" });
|
|
82416
|
+
return {
|
|
82417
|
+
ok: r.status === 0,
|
|
82418
|
+
stdout: r.stdout ?? "",
|
|
82419
|
+
stderr: r.stderr ?? ""
|
|
82420
|
+
};
|
|
82421
|
+
}
|
|
82422
|
+
async function doInstall2(opts) {
|
|
82423
|
+
const operatorUid = resolveOperatorUid();
|
|
82424
|
+
if (operatorUid === undefined) {
|
|
82425
|
+
console.error(source_default.red(`Could not resolve the operator uid (no SUDO_UID and getuid() is 0 or unavailable).
|
|
82426
|
+
` + `The web container must run as the operator uid so its webhook forwards pass
|
|
82427
|
+
` + "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."));
|
|
82428
|
+
process.exit(1);
|
|
82429
|
+
}
|
|
82430
|
+
const dir = webdDir();
|
|
82431
|
+
const composePath = webdComposePath();
|
|
82432
|
+
mkdirSync47(dir, { recursive: true });
|
|
82433
|
+
const yaml = renderWebComposeFile({
|
|
82434
|
+
hostHome: homedir47(),
|
|
82435
|
+
imageTag: opts.tag ?? DEFAULT_IMAGE_TAG2,
|
|
82436
|
+
operatorUid
|
|
82437
|
+
});
|
|
82438
|
+
if (opts.dryRun) {
|
|
82439
|
+
console.log(source_default.dim(`# Would write: ${composePath}`));
|
|
82440
|
+
console.log(yaml);
|
|
82441
|
+
console.log(source_default.dim(`# Would run: docker compose -p ${WEB_COMPOSE_PROJECT} -f ${composePath} up -d`));
|
|
82442
|
+
return;
|
|
82443
|
+
}
|
|
82444
|
+
const bak = backupExistingCompose2();
|
|
82445
|
+
if (bak)
|
|
82446
|
+
console.log(source_default.dim(` Backed up existing compose to ${bak}`));
|
|
82447
|
+
writeFileSync40(composePath, yaml, "utf8");
|
|
82448
|
+
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|
|
82449
|
+
console.log(source_default.dim(` running as uid ${operatorUid} (operator), network_mode: host`));
|
|
82450
|
+
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${opts.tag ?? DEFAULT_IMAGE_TAG2}\u2026`));
|
|
82451
|
+
const pull = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composePath, "pull"]);
|
|
82452
|
+
if (!pull.ok) {
|
|
82453
|
+
console.error(source_default.red(` pull failed:
|
|
82454
|
+
${pull.stderr}`));
|
|
82455
|
+
console.error(source_default.yellow(` Hint: \`ghcr.io/switchroom/switchroom-web:${opts.tag ?? DEFAULT_IMAGE_TAG2}\` may not be published yet.
|
|
82456
|
+
` + ` Check the docker-images workflow run and verify the tag at:
|
|
82457
|
+
` + ` https://github.com/switchroom/switchroom/pkgs/container/switchroom-web`));
|
|
82458
|
+
process.exit(1);
|
|
82459
|
+
}
|
|
82460
|
+
console.log(source_default.dim(` Bringing up web service\u2026`));
|
|
82461
|
+
const up = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composePath, "up", "-d"]);
|
|
82462
|
+
if (!up.ok) {
|
|
82463
|
+
console.error(source_default.red(` up failed:
|
|
82464
|
+
${up.stderr}`));
|
|
82465
|
+
process.exit(1);
|
|
82466
|
+
}
|
|
82467
|
+
console.log(source_default.green(` \u2713 Web service running (project: ${WEB_COMPOSE_PROJECT})`));
|
|
82468
|
+
console.log(source_default.dim(` Logs: docker logs switchroom-web --tail 50`));
|
|
82469
|
+
console.log(source_default.dim(` Verify: switchroom webd status`));
|
|
82470
|
+
console.log(source_default.yellow(` NOTE: the switchroom-web.service systemd unit (if still installed) also
|
|
82471
|
+
` + ` binds 127.0.0.1:8080. Stop it before this container can claim the port:
|
|
82472
|
+
` + ` systemctl --user stop switchroom-web.service
|
|
82473
|
+
` + ` systemctl --user disable switchroom-web.service`));
|
|
82474
|
+
}
|
|
82475
|
+
function doStatus2() {
|
|
82476
|
+
const composeYml = webdComposePath();
|
|
82477
|
+
console.log(source_default.bold("switchroom-web"));
|
|
82478
|
+
console.log("");
|
|
82479
|
+
if (!existsSync84(composeYml)) {
|
|
82480
|
+
console.log(source_default.yellow(" compose: not installed"));
|
|
82481
|
+
console.log(source_default.dim(" run `switchroom webd install` to set up."));
|
|
82482
|
+
return;
|
|
82483
|
+
}
|
|
82484
|
+
console.log(source_default.green(` compose: ${composeYml}`));
|
|
82485
|
+
const ps = runDocker2([
|
|
82486
|
+
"compose",
|
|
82487
|
+
"-p",
|
|
82488
|
+
WEB_COMPOSE_PROJECT,
|
|
82489
|
+
"-f",
|
|
82490
|
+
composeYml,
|
|
82491
|
+
"ps",
|
|
82492
|
+
"--format",
|
|
82493
|
+
"{{.Name}} {{.Status}} {{.Image}}"
|
|
82494
|
+
]);
|
|
82495
|
+
if (!ps.ok || !ps.stdout.trim()) {
|
|
82496
|
+
console.log(source_default.yellow(" container: not running"));
|
|
82497
|
+
} else {
|
|
82498
|
+
console.log(source_default.green(` container: ${ps.stdout.trim()}`));
|
|
82499
|
+
}
|
|
82500
|
+
}
|
|
82501
|
+
function doUninstall2() {
|
|
82502
|
+
const composeYml = webdComposePath();
|
|
82503
|
+
if (!existsSync84(composeYml)) {
|
|
82504
|
+
console.log(source_default.yellow(" No web-service install detected (no compose file at this path)."));
|
|
82505
|
+
return;
|
|
82506
|
+
}
|
|
82507
|
+
console.log(source_default.dim(` Stopping ${WEB_COMPOSE_PROJECT}\u2026`));
|
|
82508
|
+
const down = runDocker2(["compose", "-p", WEB_COMPOSE_PROJECT, "-f", composeYml, "down"]);
|
|
82509
|
+
if (!down.ok) {
|
|
82510
|
+
console.error(source_default.red(` down failed:
|
|
82511
|
+
${down.stderr}`));
|
|
82512
|
+
process.exit(1);
|
|
82513
|
+
}
|
|
82514
|
+
console.log(source_default.green(" \u2713 Web service stopped"));
|
|
82515
|
+
console.log(source_default.dim(` Compose file left in place at ${composeYml}`));
|
|
82516
|
+
console.log(source_default.dim(` To re-enable: switchroom webd install`));
|
|
82517
|
+
console.log(source_default.yellow(` If you cut over from the systemd unit, re-enable it to restore the
|
|
82518
|
+
` + ` dashboard + webhook receiver:
|
|
82519
|
+
` + ` systemctl --user enable --now switchroom-web.service`));
|
|
82520
|
+
}
|
|
82521
|
+
function registerWebdCommand(program3) {
|
|
82522
|
+
const webd = program3.command("webd").description("Manage switchroom-web, the dashboard + GitHub-webhook receiver container");
|
|
82523
|
+
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) => {
|
|
82524
|
+
await doInstall2(opts);
|
|
82525
|
+
}));
|
|
82526
|
+
webd.command("status").description("Show web-service container state").action(() => doStatus2());
|
|
82527
|
+
webd.command("uninstall").description("Stop the web container. Leaves the compose file in place for re-install.").action(() => doUninstall2());
|
|
82528
|
+
}
|
|
82529
|
+
|
|
82288
82530
|
// src/cli/index.ts
|
|
82289
82531
|
installGlobalErrorHandlers();
|
|
82290
82532
|
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 +82575,7 @@ registerSkillSearchCommand(program3);
|
|
|
82333
82575
|
registerAgentConfigMcpCommand(program3);
|
|
82334
82576
|
registerHostdMcpCommand(program3);
|
|
82335
82577
|
registerHostdCommand(program3);
|
|
82578
|
+
registerWebdCommand(program3);
|
|
82336
82579
|
|
|
82337
82580
|
// bin/switchroom.ts
|
|
82338
82581
|
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({
|
|
@@ -20188,6 +20192,7 @@ import { mkdtempSync, writeFileSync as writeFileSync2, rmSync, existsSync as exi
|
|
|
20188
20192
|
import { tmpdir } from "node:os";
|
|
20189
20193
|
import { join as join2, isAbsolute as isAbsolute2, normalize } from "node:path";
|
|
20190
20194
|
import { spawnSync } from "node:child_process";
|
|
20195
|
+
import { isDeepStrictEqual } from "node:util";
|
|
20191
20196
|
var MAX_PATCH_BYTES = 1024 * 1024;
|
|
20192
20197
|
var UNLOCK_CARD_YAML_ALLOWLIST = new Set([
|
|
20193
20198
|
"hostd.config_edit_enabled"
|
|
@@ -20524,6 +20529,68 @@ function validateConfigEdit(opts) {
|
|
|
20524
20529
|
return leakErr;
|
|
20525
20530
|
return { ok: true, postApplyContent: applied.after };
|
|
20526
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
|
+
}
|
|
20527
20594
|
|
|
20528
20595
|
// src/host-control/server.ts
|
|
20529
20596
|
function resolveDigests(imageRefs) {
|
|
@@ -20873,7 +20940,7 @@ class HostdServer {
|
|
|
20873
20940
|
case "doctor":
|
|
20874
20941
|
return callerAdmin ? null : `doctor requires admin: true on caller "${caller.name}"`;
|
|
20875
20942
|
case "config_propose_edit":
|
|
20876
|
-
return
|
|
20943
|
+
return null;
|
|
20877
20944
|
}
|
|
20878
20945
|
}
|
|
20879
20946
|
async handleAgentRestart(req, caller, started) {
|
|
@@ -21134,6 +21201,18 @@ class HostdServer {
|
|
|
21134
21201
|
if (!verdict.ok) {
|
|
21135
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);
|
|
21136
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
|
+
}
|
|
21137
21216
|
if (!this.opts.approvalGateway) {
|
|
21138
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", [
|
|
21139
21218
|
"ensure hostd was launched with --approval-gateway / telegram-plugin link"
|
|
@@ -21922,13 +22001,12 @@ async function main() {
|
|
|
21922
22001
|
process.exit(2);
|
|
21923
22002
|
}
|
|
21924
22003
|
const agentUids = {};
|
|
21925
|
-
for (const
|
|
21926
|
-
|
|
21927
|
-
agentUids[name] = allocateAgentUid(name);
|
|
21928
|
-
}
|
|
22004
|
+
for (const name of Object.keys(config.agents)) {
|
|
22005
|
+
agentUids[name] = allocateAgentUid(name);
|
|
21929
22006
|
}
|
|
21930
22007
|
if (Object.keys(agentUids).length === 0) {
|
|
21931
|
-
process.stderr.write(
|
|
22008
|
+
process.stderr.write(`hostd: no agents configured — nothing to serve.
|
|
22009
|
+
`);
|
|
21932
22010
|
process.exit(2);
|
|
21933
22011
|
}
|
|
21934
22012
|
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join4(homedir3(), ".switchroom", "agents");
|
|
@@ -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({
|