switchroom 0.18.31 → 0.18.33
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 -2
- package/dist/auth-broker/index.js +21 -3
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +1410 -852
- package/dist/host-control/main.js +22 -4
- package/dist/vault/approvals/kernel-server.js +21 -3
- package/dist/vault/broker/server.js +48 -4
- package/package.json +4 -3
- package/profiles/_base/start.sh.hbs +148 -23
- package/telegram-plugin/dist/gateway/gateway.js +62726 -58282
- package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
- package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
- package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
- package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
- package/telegram-plugin/gateway/boot-card.ts +169 -1
- package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
- package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
- package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
- package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
- package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
- package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
- package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
- package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
- package/telegram-plugin/gateway/gateway.ts +6403 -13456
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
- package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
- package/telegram-plugin/gateway/inbound-router.ts +400 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
- package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
- package/telegram-plugin/gateway/model-command.ts +23 -0
- package/telegram-plugin/gateway/narrative-lane.ts +865 -0
- package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
- package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
- package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
- package/telegram-plugin/gateway/secret-request-card.ts +45 -0
- package/telegram-plugin/gateway/stream-render.ts +2166 -0
- package/telegram-plugin/gateway/turn-end.ts +606 -0
- package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
- package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
- package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
- package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
- package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
- package/telegram-plugin/narrative-dedup.ts +24 -1
- package/telegram-plugin/narrative-flush.ts +2 -2
- package/telegram-plugin/pending-user-notice.ts +59 -13
- package/telegram-plugin/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +297 -31
- package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
- package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
- package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
- package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
- package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
- package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
- package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +28 -9
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +21 -12
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
- package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
- package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +76 -29
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +45 -16
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +270 -0
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +150 -0
- package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +299 -0
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +18 -5
- package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
- package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
- package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +18 -7
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
- package/telegram-plugin/tests/model-command.test.ts +30 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +36 -12
- package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
- package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
- package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
- package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
- package/telegram-plugin/tests/obligation-store.test.ts +43 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +18 -8
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
- package/telegram-plugin/tests/render/render.test.ts +42 -0
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +31 -20
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
- package/telegram-plugin/tests/stop-command.test.ts +22 -12
- package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
- package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +305 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +191 -11
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +117 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
- package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
- package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
- package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
- package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +236 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +110 -38
- package/telegram-plugin/turn-flush-safety.ts +80 -14
- package/telegram-plugin/uat/restart-capability.ts +76 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
- package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
- package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
- package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
- package/telegram-plugin/worker-activity-feed.ts +34 -4
- package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
- package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
- package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
- package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
|
@@ -19265,7 +19265,8 @@ var MemoryBackendConfigSchema = exports_external.object({
|
|
|
19265
19265
|
llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
|
|
19266
19266
|
api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
|
|
19267
19267
|
docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
|
|
19268
|
-
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
|
|
19268
|
+
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
|
|
19269
|
+
mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
|
|
19269
19270
|
}).optional()
|
|
19270
19271
|
});
|
|
19271
19272
|
var VaultConfigSchema = exports_external.object({
|
|
@@ -19308,7 +19309,8 @@ var HostControlConfigSchema = exports_external.object({
|
|
|
19308
19309
|
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.")
|
|
19309
19310
|
});
|
|
19310
19311
|
var WebServiceConfigSchema = exports_external.object({
|
|
19311
|
-
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.")
|
|
19312
|
+
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."),
|
|
19313
|
+
port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
|
|
19312
19314
|
});
|
|
19313
19315
|
var FleetHealthConfigSchema = exports_external.object({
|
|
19314
19316
|
owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
@@ -20685,7 +20687,23 @@ var AUDIT_ROOT = join2(homedir2(), ".switchroom", "audit");
|
|
|
20685
20687
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
20686
20688
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
|
|
20687
20689
|
import { resolve as resolve4, join as join3, sep as pathSep } from "node:path";
|
|
20688
|
-
|
|
20690
|
+
function resolveProfilesRoot() {
|
|
20691
|
+
const envOverride = process.env.SWITCHROOM_PROFILES_ROOT?.trim();
|
|
20692
|
+
if (envOverride) {
|
|
20693
|
+
return resolve4(envOverride);
|
|
20694
|
+
}
|
|
20695
|
+
const candidates = [
|
|
20696
|
+
resolve4(import.meta.dirname, "../../profiles"),
|
|
20697
|
+
resolve4(import.meta.dirname, "profiles")
|
|
20698
|
+
];
|
|
20699
|
+
for (const candidate of candidates) {
|
|
20700
|
+
if (existsSync5(candidate)) {
|
|
20701
|
+
return candidate;
|
|
20702
|
+
}
|
|
20703
|
+
}
|
|
20704
|
+
return candidates[0];
|
|
20705
|
+
}
|
|
20706
|
+
var PROFILES_ROOT = resolveProfilesRoot();
|
|
20689
20707
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
20690
20708
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
20691
20709
|
});
|
|
@@ -26645,7 +26663,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
|
|
|
26645
26663
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
26646
26664
|
|
|
26647
26665
|
// src/build-info.ts
|
|
26648
|
-
var VERSION = "0.18.
|
|
26666
|
+
var VERSION = "0.18.33";
|
|
26649
26667
|
|
|
26650
26668
|
// src/cli/resolve-version.ts
|
|
26651
26669
|
function readPackageVersion() {
|
|
@@ -4685,7 +4685,8 @@ var init_schema = __esm(() => {
|
|
|
4685
4685
|
llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
|
|
4686
4686
|
api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
|
|
4687
4687
|
docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
|
|
4688
|
-
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
|
|
4688
|
+
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
|
|
4689
|
+
mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
|
|
4689
4690
|
}).optional()
|
|
4690
4691
|
});
|
|
4691
4692
|
VaultConfigSchema = exports_external.object({
|
|
@@ -4728,7 +4729,8 @@ var init_schema = __esm(() => {
|
|
|
4728
4729
|
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.")
|
|
4729
4730
|
});
|
|
4730
4731
|
WebServiceConfigSchema = exports_external.object({
|
|
4731
|
-
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.")
|
|
4732
|
+
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."),
|
|
4733
|
+
port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
|
|
4732
4734
|
});
|
|
4733
4735
|
FleetHealthConfigSchema = exports_external.object({
|
|
4734
4736
|
owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
@@ -19070,7 +19072,23 @@ var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
|
|
|
19070
19072
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
19071
19073
|
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
19072
19074
|
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
19073
|
-
|
|
19075
|
+
function resolveProfilesRoot() {
|
|
19076
|
+
const envOverride = process.env.SWITCHROOM_PROFILES_ROOT?.trim();
|
|
19077
|
+
if (envOverride) {
|
|
19078
|
+
return resolve4(envOverride);
|
|
19079
|
+
}
|
|
19080
|
+
const candidates = [
|
|
19081
|
+
resolve4(import.meta.dirname, "../../profiles"),
|
|
19082
|
+
resolve4(import.meta.dirname, "profiles")
|
|
19083
|
+
];
|
|
19084
|
+
for (const candidate of candidates) {
|
|
19085
|
+
if (existsSync4(candidate)) {
|
|
19086
|
+
return candidate;
|
|
19087
|
+
}
|
|
19088
|
+
}
|
|
19089
|
+
return candidates[0];
|
|
19090
|
+
}
|
|
19091
|
+
var PROFILES_ROOT = resolveProfilesRoot();
|
|
19074
19092
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
19075
19093
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
19076
19094
|
});
|
|
@@ -4685,7 +4685,8 @@ var init_schema = __esm(() => {
|
|
|
4685
4685
|
llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
|
|
4686
4686
|
api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
|
|
4687
4687
|
docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
|
|
4688
|
-
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
|
|
4688
|
+
url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
|
|
4689
|
+
mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
|
|
4689
4690
|
}).optional()
|
|
4690
4691
|
});
|
|
4691
4692
|
VaultConfigSchema = exports_external.object({
|
|
@@ -4728,7 +4729,8 @@ var init_schema = __esm(() => {
|
|
|
4728
4729
|
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.")
|
|
4729
4730
|
});
|
|
4730
4731
|
WebServiceConfigSchema = exports_external.object({
|
|
4731
|
-
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.")
|
|
4732
|
+
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."),
|
|
4733
|
+
port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
|
|
4732
4734
|
});
|
|
4733
4735
|
FleetHealthConfigSchema = exports_external.object({
|
|
4734
4736
|
owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
@@ -18778,7 +18780,23 @@ var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
|
|
|
18778
18780
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
18779
18781
|
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
18780
18782
|
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
18781
|
-
|
|
18783
|
+
function resolveProfilesRoot() {
|
|
18784
|
+
const envOverride = process.env.SWITCHROOM_PROFILES_ROOT?.trim();
|
|
18785
|
+
if (envOverride) {
|
|
18786
|
+
return resolve4(envOverride);
|
|
18787
|
+
}
|
|
18788
|
+
const candidates = [
|
|
18789
|
+
resolve4(import.meta.dirname, "../../profiles"),
|
|
18790
|
+
resolve4(import.meta.dirname, "profiles")
|
|
18791
|
+
];
|
|
18792
|
+
for (const candidate of candidates) {
|
|
18793
|
+
if (existsSync4(candidate)) {
|
|
18794
|
+
return candidate;
|
|
18795
|
+
}
|
|
18796
|
+
}
|
|
18797
|
+
return candidates[0];
|
|
18798
|
+
}
|
|
18799
|
+
var PROFILES_ROOT = resolveProfilesRoot();
|
|
18782
18800
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
18783
18801
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
18784
18802
|
});
|
|
@@ -23427,6 +23445,23 @@ class VaultBroker {
|
|
|
23427
23445
|
socket.write(encodeResponse(errorResponse("DENIED", aclResult.reason)));
|
|
23428
23446
|
return;
|
|
23429
23447
|
}
|
|
23448
|
+
const existingForScope = this.secrets[req.key];
|
|
23449
|
+
if (existingForScope !== undefined) {
|
|
23450
|
+
const writeScope = checkEntryScope(existingForScope.scope, agentName);
|
|
23451
|
+
if (!writeScope.allow) {
|
|
23452
|
+
this.auditLogger.write({
|
|
23453
|
+
ts: new Date().toISOString(),
|
|
23454
|
+
op: "put",
|
|
23455
|
+
key: req.key,
|
|
23456
|
+
caller: auditCaller,
|
|
23457
|
+
pid: auditPid,
|
|
23458
|
+
cgroup: auditCgroup,
|
|
23459
|
+
result: `denied:${writeScope.reason}`
|
|
23460
|
+
});
|
|
23461
|
+
socket.write(encodeResponse(errorResponse("DENIED", writeScope.reason)));
|
|
23462
|
+
return;
|
|
23463
|
+
}
|
|
23464
|
+
}
|
|
23430
23465
|
}
|
|
23431
23466
|
const existing = this.secrets[req.key];
|
|
23432
23467
|
if (existing === undefined && writeGrantId === null && !passphraseAttested) {
|
|
@@ -23456,7 +23491,16 @@ class VaultBroker {
|
|
|
23456
23491
|
return;
|
|
23457
23492
|
}
|
|
23458
23493
|
const previousEntry = existing;
|
|
23459
|
-
|
|
23494
|
+
let nextEntry = req.entry;
|
|
23495
|
+
if (existing !== undefined && !passphraseAttested) {
|
|
23496
|
+
const preservedFormat = "format" in existing && existing.format !== undefined ? { format: existing.format } : {};
|
|
23497
|
+
nextEntry = {
|
|
23498
|
+
...req.entry,
|
|
23499
|
+
...preservedFormat,
|
|
23500
|
+
...existing.scope !== undefined ? { scope: existing.scope } : {}
|
|
23501
|
+
};
|
|
23502
|
+
}
|
|
23503
|
+
this.secrets[req.key] = nextEntry;
|
|
23460
23504
|
try {
|
|
23461
23505
|
saveVault(this.passphrase, this.vaultPath, this.secrets);
|
|
23462
23506
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.33",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"pretest": "npm run build",
|
|
27
27
|
"test": "vitest run && bun test telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
28
28
|
"test:vitest": "vitest run",
|
|
29
|
-
"test:bun": "bun test telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
29
|
+
"test:bun": "bun test telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
33
33
|
"lint:plugin-references": "node scripts/check-plugin-references.mjs",
|
|
34
34
|
"lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"lint:no-pii": "node scripts/check-no-pii-secrets.mjs",
|
|
37
37
|
"lint:web-subscription-honest": "node scripts/check-web-subscription-honest.mjs",
|
|
38
38
|
"lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
|
|
39
|
+
"lint:gateway-line-ratchet": "node scripts/check-gateway-line-ratchet.mjs",
|
|
39
40
|
"prepublishOnly": "npm run build && npm run lint && npm test"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
@@ -96,6 +96,13 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
96
96
|
#
|
|
97
97
|
# SWITCHROOM_AGENT_NAME is injected by compose env (compose.ts:1816)
|
|
98
98
|
# so it is available here, before the inner-pass export at line ~320.
|
|
99
|
+
#
|
|
100
|
+
# Capture whether LiteLLM routing was configured at compose time BEFORE the
|
|
101
|
+
# missing-key fail-open below can strip SWITCHROOM_LITELLM. The routing-mode
|
|
102
|
+
# observability block (inner pass, pre-exec) reads this to distinguish
|
|
103
|
+
# "litellm never configured" (declared intent IS direct-oauth — no
|
|
104
|
+
# divergence) from "configured but stripped at boot" (real divergence).
|
|
105
|
+
export SWITCHROOM_LITELLM_DECLARED="${SWITCHROOM_LITELLM_DECLARED:-${SWITCHROOM_LITELLM:-}}"
|
|
99
106
|
if [ -n "${SWITCHROOM_LITELLM:-}" ] && [ -z "$ANTHROPIC_CUSTOM_HEADERS" ] && command -v switchroom >/dev/null 2>&1; then
|
|
100
107
|
sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
|
|
101
108
|
sr_ll_ok=""
|
|
@@ -602,16 +609,54 @@ export HINDSIGHT_SENDER_BANKS_JSON={{{hindsightSenderBanksJsonQ}}}
|
|
|
602
609
|
{{#if hindsightTopicFilterMode}}
|
|
603
610
|
export HINDSIGHT_TOPIC_FILTER_MODE={{hindsightTopicFilterMode}}
|
|
604
611
|
{{/if}}
|
|
605
|
-
#
|
|
606
|
-
#
|
|
612
|
+
# Give Claude Code a longer MCP server-startup budget (ms). The default can
|
|
613
|
+
# race a Hindsight restart window; the boot gate below usually absorbs it,
|
|
614
|
+
# but this keeps the in-session connect attempt itself patient too.
|
|
615
|
+
# Documented Claude Code env var (MCP server startup timeout).
|
|
616
|
+
export MCP_TIMEOUT="${MCP_TIMEOUT:-30000}"
|
|
617
|
+
# Wait for Hindsight MCP to be READY before launching Claude, otherwise the
|
|
618
|
+
# MCP server connection fails at startup with "1 MCP server failed" and the
|
|
619
|
+
# session boots toolless (incident 2026-07-18: clerk booted toolless despite
|
|
620
|
+
# a 30s bare-GET gate — the endpoint returned HTTP 200 mid-restart while a
|
|
621
|
+
# real MCP initialize still failed). So: probe with an actual JSON-RPC
|
|
622
|
+
# initialize POST and require a JSON-RPC result, not just HTTP 200.
|
|
623
|
+
# Bounded at 120s of real wall-clock time (each probe costs up to ~3s of
|
|
624
|
+
# curl timeout + 1s sleep, so an iteration count would overshoot — measure
|
|
625
|
+
# elapsed seconds against the deadline instead; HINDSIGHT_WAIT holds real
|
|
626
|
+
# elapsed seconds). On timeout we log a warning and boot anyway (degraded
|
|
627
|
+
# beats never-boots — the plugin reconnects once Hindsight recovers).
|
|
607
628
|
HINDSIGHT_WAIT=0
|
|
608
|
-
|
|
609
|
-
|
|
629
|
+
HINDSIGHT_READY=0
|
|
630
|
+
_hs_start=$(date +%s)
|
|
631
|
+
while [ $HINDSIGHT_WAIT -lt 120 ]; do
|
|
632
|
+
_hs_resp=$(curl -sf --max-time 3 -i \
|
|
633
|
+
-X POST {{{hindsightApiBaseUrlQ}}}/mcp/ \
|
|
634
|
+
-H "Content-Type: application/json" \
|
|
635
|
+
-H "Accept: application/json, text/event-stream" \
|
|
636
|
+
-d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"switchroom-boot-gate","version":"1.0"}}}' \
|
|
637
|
+
2>/dev/null)
|
|
638
|
+
# Match on serverInfo — only present in a successful initialize result;
|
|
639
|
+
# a JSON-RPC *error* body could legitimately contain the substring "result".
|
|
640
|
+
if printf '%s\n' "$_hs_resp" | grep -q '"serverInfo"'; then
|
|
641
|
+
HINDSIGHT_READY=1
|
|
642
|
+
# Best-effort cleanup of the probe's server-side MCP session (one would
|
|
643
|
+
# leak per boot otherwise). Header may be absent on stateless servers.
|
|
644
|
+
_hs_sid=$(printf '%s\n' "$_hs_resp" | tr -d '\r' | sed -n 's/^[Mm][Cc][Pp]-[Ss]ession-[Ii][Dd]: *//p' | head -n 1)
|
|
645
|
+
if [ -n "$_hs_sid" ]; then
|
|
646
|
+
curl -s -o /dev/null --max-time 2 -X DELETE {{{hindsightApiBaseUrlQ}}}/mcp/ \
|
|
647
|
+
-H "Mcp-Session-Id: $_hs_sid" 2>/dev/null || true
|
|
648
|
+
fi
|
|
649
|
+
HINDSIGHT_WAIT=$(( $(date +%s) - _hs_start ))
|
|
610
650
|
break
|
|
611
651
|
fi
|
|
612
652
|
sleep 1
|
|
613
|
-
HINDSIGHT_WAIT=$((
|
|
653
|
+
HINDSIGHT_WAIT=$(( $(date +%s) - _hs_start ))
|
|
614
654
|
done
|
|
655
|
+
if [ "$HINDSIGHT_READY" = "1" ]; then
|
|
656
|
+
echo "start.sh: hindsight boot gate — MCP initialize OK after ${HINDSIGHT_WAIT}s" >&2
|
|
657
|
+
else
|
|
658
|
+
echo "start.sh: WARNING hindsight boot gate TIMED OUT after ${HINDSIGHT_WAIT}s — MCP initialize never returned a JSON-RPC result; launching claude anyway (memory tools may be unavailable this session)" >&2
|
|
659
|
+
fi
|
|
615
660
|
{{/if}}
|
|
616
661
|
{{#if userEnvQuoted}}
|
|
617
662
|
# User-declared env vars from switchroom.yaml (agent + defaults merged).
|
|
@@ -1058,7 +1103,20 @@ fi
|
|
|
1058
1103
|
# unreachable at boot, or claude would 4xx an unknown model against Anthropic.
|
|
1059
1104
|
# Empty by default (proxy not confirmed live); set to "1" only on a successful
|
|
1060
1105
|
# liveliness probe. Deliberately NOT unset with the sr_ll_* scratch vars.
|
|
1106
|
+
#
|
|
1107
|
+
# _LITELLM_UNREACHABLE distinguishes WHY _LITELLM_OK is empty (routing-cause
|
|
1108
|
+
# split, 2026-07-17 boot-race incident): "1" means the key was present and the
|
|
1109
|
+
# probe failed — routing was LEFT IN PLACE pointing at the proxy, so a
|
|
1110
|
+
# proxy-only model (fable/sr-*) can boot loud-degraded and self-heal per
|
|
1111
|
+
# request. Empty means either the key is MISSING (routing stripped — a
|
|
1112
|
+
# proxy-only model genuinely cannot work and must demote) or LiteLLM is not
|
|
1113
|
+
# configured at all (same conclusion). Deliberately NOT unset either.
|
|
1061
1114
|
_LITELLM_OK=""
|
|
1115
|
+
_LITELLM_UNREACHABLE=""
|
|
1116
|
+
# Idempotent mirror of the outer-pass capture (first setter wins) — on the
|
|
1117
|
+
# non-docker path only this inner block runs, so capture here too, before the
|
|
1118
|
+
# missing-key fail-open below can strip SWITCHROOM_LITELLM.
|
|
1119
|
+
export SWITCHROOM_LITELLM_DECLARED="${SWITCHROOM_LITELLM_DECLARED:-${SWITCHROOM_LITELLM:-}}"
|
|
1062
1120
|
if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; then
|
|
1063
1121
|
sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
|
|
1064
1122
|
sr_ll_ok=""
|
|
@@ -1074,10 +1132,14 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1074
1132
|
# co-boots, the litellm proxy's heavy Python app is often not yet healthy
|
|
1075
1133
|
# in the first few seconds. A single one-shot probe fails-open and takes
|
|
1076
1134
|
# this agent dark (untracked) for the WHOLE session. Poll the liveliness
|
|
1077
|
-
# endpoint every ~3s for up to
|
|
1135
|
+
# endpoint every ~3s for up to SWITCHROOM_LITELLM_BOOT_BUDGET seconds
|
|
1136
|
+
# (default 240 — the INNER budget only; the outer/gateway-pass probe stays
|
|
1137
|
+
# at 120s so gateway boot latency is not doubled) and only fall open if
|
|
1078
1138
|
# it is STILL unreachable after that whole window.
|
|
1079
1139
|
sr_ll_url="${SWITCHROOM_LITELLM_BASE:-${ANTHROPIC_BASE_URL%/anthropic}}/health/liveliness"
|
|
1080
|
-
|
|
1140
|
+
sr_ll_budget="${SWITCHROOM_LITELLM_BOOT_BUDGET:-240}"
|
|
1141
|
+
case "$sr_ll_budget" in *[!0-9]*|"") sr_ll_budget=240 ;; esac
|
|
1142
|
+
sr_ll_deadline=$(( $(date +%s) + sr_ll_budget ))
|
|
1081
1143
|
sr_ll_up=""
|
|
1082
1144
|
while :; do
|
|
1083
1145
|
if curl -fsS -m 5 -o /dev/null "$sr_ll_url" 2>/dev/null; then sr_ll_up="1"; break; fi
|
|
@@ -1089,8 +1151,9 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1089
1151
|
# leave it pointed at litellm so the fleet self-heals once the proxy is
|
|
1090
1152
|
# back (the socat forwarder at 127.0.0.1:4010 reconnects per-connection).
|
|
1091
1153
|
sr_ll_unreachable="1"
|
|
1154
|
+
_LITELLM_UNREACHABLE="1"
|
|
1092
1155
|
echo "==================== litellm UNREACHABLE ====================" >&2
|
|
1093
|
-
echo "WARNING: litellm proxy unreachable at ${SWITCHROOM_LITELLM_BASE:-$ANTHROPIC_BASE_URL} after
|
|
1156
|
+
echo "WARNING: litellm proxy unreachable at ${SWITCHROOM_LITELLM_BASE:-$ANTHROPIC_BASE_URL} after ${sr_ll_budget}s of retries;" >&2
|
|
1094
1157
|
echo "WARNING: Claude traffic will FAIL until it recovers — routing LEFT IN PLACE" >&2
|
|
1095
1158
|
echo "WARNING: so it self-heals (socat forwarder reconnects per-connection)." >&2
|
|
1096
1159
|
echo "WARNING: NOT falling back to untracked direct Anthropic OAuth." >&2
|
|
@@ -1098,7 +1161,7 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1098
1161
|
else
|
|
1099
1162
|
sr_ll_ok="1"
|
|
1100
1163
|
fi
|
|
1101
|
-
unset sr_ll_url sr_ll_deadline sr_ll_up
|
|
1164
|
+
unset sr_ll_url sr_ll_budget sr_ll_deadline sr_ll_up
|
|
1102
1165
|
else
|
|
1103
1166
|
sr_ll_ok="1"
|
|
1104
1167
|
fi
|
|
@@ -1381,22 +1444,37 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1381
1444
|
unset _smf _sm_model _sm_cfg _sm_attempts _SM_MAX_ATTEMPTS
|
|
1382
1445
|
fi
|
|
1383
1446
|
|
|
1384
|
-
# Configured-default LiteLLM guard
|
|
1385
|
-
#
|
|
1386
|
-
#
|
|
1387
|
-
#
|
|
1388
|
-
#
|
|
1389
|
-
#
|
|
1390
|
-
#
|
|
1391
|
-
# safe same-class fallback
|
|
1392
|
-
#
|
|
1393
|
-
#
|
|
1447
|
+
# Configured-default LiteLLM guard, SPLIT BY CAUSE (2026-07-17 boot-race
|
|
1448
|
+
# incident: a slow LiteLLM co-boot demoted a configured-fable agent to opus on
|
|
1449
|
+
# the passthrough — silent divergence that persisted for the whole session).
|
|
1450
|
+
#
|
|
1451
|
+
# - MISSING KEY / LiteLLM not configured (_LITELLM_UNREACHABLE empty):
|
|
1452
|
+
# routing was STRIPPED (direct Anthropic OAuth), where `fable`/
|
|
1453
|
+
# `claude-fable-5` is a retired codename and EVERY call 4xxs — a total
|
|
1454
|
+
# outage. Claude agents have a safe same-class fallback: demote to opus,
|
|
1455
|
+
# loudly (stderr + .session-model-alert).
|
|
1456
|
+
# - PROXY UNREACHABLE, key present (_LITELLM_UNREACHABLE="1"): routing was
|
|
1457
|
+
# LEFT IN PLACE pointing at the proxy. Do NOT demote — boot the declared
|
|
1458
|
+
# fable model against the LiteLLM router root (the repoint case below
|
|
1459
|
+
# includes the unreachable path) and run loud-degraded, the same policy as
|
|
1460
|
+
# sr-* configured defaults: requests fail until the proxy heals, then work
|
|
1461
|
+
# with ZERO divergence and no restart needed (per-request recovery — see
|
|
1462
|
+
# the repoint comment below).
|
|
1463
|
+
#
|
|
1464
|
+
# The carrier block above already drops proxy-only OVERRIDES on a down proxy;
|
|
1465
|
+
# this guard covers only the configured default, which the live-config read
|
|
1466
|
+
# above makes a mainline path (pinning `model: fable` in yaml is the normal
|
|
1467
|
+
# way to run Fable).
|
|
1394
1468
|
case "$_EFFECTIVE_MODEL" in
|
|
1395
1469
|
fable|claude-fable-5)
|
|
1396
1470
|
if [ -z "$_LITELLM_OK" ]; then
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1471
|
+
if [ -n "$_LITELLM_UNREACHABLE" ]; then
|
|
1472
|
+
echo "session-model: configured model '$_EFFECTIVE_MODEL' is proxy-only and LiteLLM is unreachable at boot — booting '$_EFFECTIVE_MODEL' anyway against the LiteLLM router (loud-degraded; requests fail until the proxy recovers, then self-heal per-request). NOT demoting to opus." >&2
|
|
1473
|
+
else
|
|
1474
|
+
echo "session-model: configured model '$_EFFECTIVE_MODEL' is proxy-only but no LiteLLM virtual key is available (routing stripped to direct Anthropic, where it would 4xx every call) — booting 'opus' instead" >&2
|
|
1475
|
+
printf 'No LiteLLM virtual key was available at boot (routing stripped to direct Anthropic), so the configured model `%s` (proxy-only) cannot work — the agent booted on `opus` instead. Run `switchroom apply` on the host (it provisions the LiteLLM key), then restart this agent to return to `%s`.\n' "$_EFFECTIVE_MODEL" "$_EFFECTIVE_MODEL" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1476
|
+
_EFFECTIVE_MODEL="opus"
|
|
1477
|
+
fi
|
|
1400
1478
|
fi
|
|
1401
1479
|
;;
|
|
1402
1480
|
esac
|
|
@@ -1480,9 +1558,16 @@ printf '%s\n' "$_EFFECTIVE_EFFORT" > "{{agentDir}}/.active-session-effort" 2>/de
|
|
|
1480
1558
|
# /v1/models (both `fable` and `claude-fable-5` served). Other Claude sessions
|
|
1481
1559
|
# (opus/sonnet/haiku/default) keep the passthrough — it dodges the Opus SSE
|
|
1482
1560
|
# re-chunk stall; Fable trades that for being routable at all.
|
|
1561
|
+
# Repoint fires when the proxy is LIVE (_LITELLM_OK) OR when it is unreachable
|
|
1562
|
+
# with the key present (_LITELLM_UNREACHABLE — routing left in place, cause
|
|
1563
|
+
# split above): a configured fable that survives the unreachable guard must
|
|
1564
|
+
# land on the ROUTER ROOT, not the model-agnostic passthrough, or it would
|
|
1565
|
+
# keep 4xxing even after the proxy heals. Missing-key boots never get here
|
|
1566
|
+
# with a proxy-only model (the guard above demoted fable to opus, and sr-*
|
|
1567
|
+
# had its routing stripped anyway).
|
|
1483
1568
|
case "$_EFFECTIVE_MODEL" in
|
|
1484
1569
|
sr-*|fable|claude-fable-5)
|
|
1485
|
-
if [ -n "$_LITELLM_OK" ] && [ -n "$SWITCHROOM_LITELLM_BASE" ]; then
|
|
1570
|
+
if { [ -n "$_LITELLM_OK" ] || [ -n "$_LITELLM_UNREACHABLE" ]; } && [ -n "$SWITCHROOM_LITELLM_BASE" ]; then
|
|
1486
1571
|
export ANTHROPIC_BASE_URL="$SWITCHROOM_LITELLM_BASE"
|
|
1487
1572
|
echo "session-model: effective model '$_EFFECTIVE_MODEL' is proxy-only (sr-*/fable) — routing via LiteLLM model router '$SWITCHROOM_LITELLM_BASE' (off /anthropic passthrough)" >&2
|
|
1488
1573
|
fi
|
|
@@ -1493,6 +1578,46 @@ esac
|
|
|
1493
1578
|
# welcome card honest. Overwrite (not consumed) — the gateway reads it at boot.
|
|
1494
1579
|
printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.active-session-model" 2>/dev/null || true
|
|
1495
1580
|
|
|
1581
|
+
# --- Routing-mode observability (.routing-mode, 2026-07-17 boot-race incident) ---
|
|
1582
|
+
#
|
|
1583
|
+
# Deterministic record of WHERE this boot actually landed, written immediately
|
|
1584
|
+
# before exec claude (overwrite-per-boot, one line). Consumers: the gateway
|
|
1585
|
+
# boot card's "routing" row and any operator shell. _DECLARED_ROUTING is baked
|
|
1586
|
+
# at render time by declaredRoutingMode() — the POST-REPOINT mode this boot is
|
|
1587
|
+
# meant to land on (fable/claude-fable-5/sr-* declare router-root because the
|
|
1588
|
+
# repoint case below moves them there; only non-fable Claude models declare
|
|
1589
|
+
# passthrough) — the apply-time INTENT the landed mode is compared against.
|
|
1590
|
+
# Divergence (landed ≠ declared) appends a consume-once .session-model-alert
|
|
1591
|
+
# line the gateway relays to the operator, but ONLY when the (landed, declared)
|
|
1592
|
+
# PAIR differs from the PREVIOUS boot's pair — a persistently degraded agent
|
|
1593
|
+
# alerts once, not on every restart, yet a declared-only flip (operator changes
|
|
1594
|
+
# the configured model class while the agent stays stuck on the same wrong
|
|
1595
|
+
# landed mode) still re-alerts.
|
|
1596
|
+
_DECLARED_ROUTING={{{declaredRoutingQ}}}
|
|
1597
|
+
# When LiteLLM was never configured for this agent (no SWITCHROOM_LITELLM at
|
|
1598
|
+
# boot entry, captured before any strip), compose injects NO routing env — the
|
|
1599
|
+
# declared intent genuinely IS direct-oauth, not the model-class bake. This
|
|
1600
|
+
# keeps non-LiteLLM fleets from false-alerting divergence on every boot.
|
|
1601
|
+
if [ -z "${SWITCHROOM_LITELLM_DECLARED:-}" ]; then _DECLARED_ROUTING="direct-oauth"; fi
|
|
1602
|
+
case "${ANTHROPIC_BASE_URL:-}" in
|
|
1603
|
+
"") _ROUTING_MODE="direct-oauth" ;;
|
|
1604
|
+
*/anthropic) _ROUTING_MODE="passthrough" ;;
|
|
1605
|
+
*) _ROUTING_MODE="router-root" ;;
|
|
1606
|
+
esac
|
|
1607
|
+
_prev_routing_mode="$(sed -n 's/^mode=\([^ ]*\).*/\1/p' "{{agentDir}}/.routing-mode" 2>/dev/null | head -n 1 || true)"
|
|
1608
|
+
_prev_declared_routing="$(sed -n 's/.* declared=\([^ ]*\).*/\1/p' "{{agentDir}}/.routing-mode" 2>/dev/null | head -n 1 || true)"
|
|
1609
|
+
_routing_line="mode=$_ROUTING_MODE base=${ANTHROPIC_BASE_URL:-direct} model=$_EFFECTIVE_MODEL litellm_ok=${_LITELLM_OK:-0} declared=$_DECLARED_ROUTING ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1610
|
+
printf '%s\n' "$_routing_line" > "{{agentDir}}/.routing-mode" 2>/dev/null || true
|
|
1611
|
+
echo "routing: $_routing_line" >&2
|
|
1612
|
+
# Dedup on the (landed, declared) PAIR, not landed alone: a persistently
|
|
1613
|
+
# degraded agent alerts once, but if the operator flips the declared class
|
|
1614
|
+
# while the agent stays stuck on the same wrong landed mode, the pair changes
|
|
1615
|
+
# and we re-alert (LOW-2).
|
|
1616
|
+
if [ "$_ROUTING_MODE" != "$_DECLARED_ROUTING" ] && { [ "$_ROUTING_MODE" != "$_prev_routing_mode" ] || [ "$_DECLARED_ROUTING" != "$_prev_declared_routing" ]; }; then
|
|
1617
|
+
printf 'Routing divergence: this agent landed on `%s` (base `%s`, model `%s`) but its declared routing is `%s`. Check `%s/.routing-mode` and the boot log for the cause (LiteLLM key/provision or proxy reachability).\n' "$_ROUTING_MODE" "${ANTHROPIC_BASE_URL:-direct}" "$_EFFECTIVE_MODEL" "$_DECLARED_ROUTING" "{{agentDir}}" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1618
|
+
fi
|
|
1619
|
+
unset _prev_routing_mode _prev_declared_routing _routing_line
|
|
1620
|
+
|
|
1496
1621
|
{{#if useSwitchroomPlugin}}
|
|
1497
1622
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
1498
1623
|
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL" $_EFFORT_ARG{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|