switchroom 0.18.32 → 0.19.0
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/auth-broker/index.js +17 -1
- package/dist/cli/switchroom.js +847 -729
- package/dist/host-control/main.js +18 -2
- package/dist/vault/approvals/kernel-server.js +17 -1
- package/dist/vault/broker/server.js +44 -2
- package/package.json +2 -2
- package/profiles/_base/start.sh.hbs +105 -18
- package/telegram-plugin/dist/gateway/gateway.js +60612 -56998
- 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/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/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/gateway.ts +1347 -6758
- 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-wiring.ts +333 -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/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +186 -3
- 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/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 +15 -6
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +18 -9
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -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 +47 -10
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +27 -9
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +39 -18
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +160 -0
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +3 -7
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +8 -2
- 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-emit-after-intercepts.test.ts +14 -3
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- 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 +27 -9
- 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/pending-card-durability-wiring.test.ts +16 -7
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- 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/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 +28 -18
- 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 +14 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-flush-safety.test.ts +183 -5
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +9 -4
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -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 +110 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +83 -35
- 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 +10 -4
|
@@ -20687,7 +20687,23 @@ var AUDIT_ROOT = join2(homedir2(), ".switchroom", "audit");
|
|
|
20687
20687
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
20688
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";
|
|
20689
20689
|
import { resolve as resolve4, join as join3, sep as pathSep } from "node:path";
|
|
20690
|
-
|
|
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();
|
|
20691
20707
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
20692
20708
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
20693
20709
|
});
|
|
@@ -26647,7 +26663,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
|
|
|
26647
26663
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
26648
26664
|
|
|
26649
26665
|
// src/build-info.ts
|
|
26650
|
-
var VERSION = "0.
|
|
26666
|
+
var VERSION = "0.19.0";
|
|
26651
26667
|
|
|
26652
26668
|
// src/cli/resolve-version.ts
|
|
26653
26669
|
function readPackageVersion() {
|
|
@@ -19072,7 +19072,23 @@ var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
|
|
|
19072
19072
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
19073
19073
|
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
19074
19074
|
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
19075
|
-
|
|
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();
|
|
19076
19092
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
19077
19093
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
19078
19094
|
});
|
|
@@ -18780,7 +18780,23 @@ var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
|
|
|
18780
18780
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
18781
18781
|
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
18782
18782
|
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
18783
|
-
|
|
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();
|
|
18784
18800
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
18785
18801
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
18786
18802
|
});
|
|
@@ -23429,6 +23445,23 @@ class VaultBroker {
|
|
|
23429
23445
|
socket.write(encodeResponse(errorResponse("DENIED", aclResult.reason)));
|
|
23430
23446
|
return;
|
|
23431
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
|
+
}
|
|
23432
23465
|
}
|
|
23433
23466
|
const existing = this.secrets[req.key];
|
|
23434
23467
|
if (existing === undefined && writeGrantId === null && !passphraseAttested) {
|
|
@@ -23458,7 +23491,16 @@ class VaultBroker {
|
|
|
23458
23491
|
return;
|
|
23459
23492
|
}
|
|
23460
23493
|
const previousEntry = existing;
|
|
23461
|
-
|
|
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;
|
|
23462
23504
|
try {
|
|
23463
23505
|
saveVault(this.passphrase, this.vaultPath, this.secrets);
|
|
23464
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.
|
|
4
|
+
"version": "0.19.0",
|
|
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,7 +26,7 @@
|
|
|
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
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",
|
|
@@ -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=""
|
|
@@ -1096,7 +1103,20 @@ fi
|
|
|
1096
1103
|
# unreachable at boot, or claude would 4xx an unknown model against Anthropic.
|
|
1097
1104
|
# Empty by default (proxy not confirmed live); set to "1" only on a successful
|
|
1098
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.
|
|
1099
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:-}}"
|
|
1100
1120
|
if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; then
|
|
1101
1121
|
sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
|
|
1102
1122
|
sr_ll_ok=""
|
|
@@ -1112,10 +1132,14 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1112
1132
|
# co-boots, the litellm proxy's heavy Python app is often not yet healthy
|
|
1113
1133
|
# in the first few seconds. A single one-shot probe fails-open and takes
|
|
1114
1134
|
# this agent dark (untracked) for the WHOLE session. Poll the liveliness
|
|
1115
|
-
# 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
|
|
1116
1138
|
# it is STILL unreachable after that whole window.
|
|
1117
1139
|
sr_ll_url="${SWITCHROOM_LITELLM_BASE:-${ANTHROPIC_BASE_URL%/anthropic}}/health/liveliness"
|
|
1118
|
-
|
|
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 ))
|
|
1119
1143
|
sr_ll_up=""
|
|
1120
1144
|
while :; do
|
|
1121
1145
|
if curl -fsS -m 5 -o /dev/null "$sr_ll_url" 2>/dev/null; then sr_ll_up="1"; break; fi
|
|
@@ -1127,8 +1151,9 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1127
1151
|
# leave it pointed at litellm so the fleet self-heals once the proxy is
|
|
1128
1152
|
# back (the socat forwarder at 127.0.0.1:4010 reconnects per-connection).
|
|
1129
1153
|
sr_ll_unreachable="1"
|
|
1154
|
+
_LITELLM_UNREACHABLE="1"
|
|
1130
1155
|
echo "==================== litellm UNREACHABLE ====================" >&2
|
|
1131
|
-
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
|
|
1132
1157
|
echo "WARNING: Claude traffic will FAIL until it recovers — routing LEFT IN PLACE" >&2
|
|
1133
1158
|
echo "WARNING: so it self-heals (socat forwarder reconnects per-connection)." >&2
|
|
1134
1159
|
echo "WARNING: NOT falling back to untracked direct Anthropic OAuth." >&2
|
|
@@ -1136,7 +1161,7 @@ if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; th
|
|
|
1136
1161
|
else
|
|
1137
1162
|
sr_ll_ok="1"
|
|
1138
1163
|
fi
|
|
1139
|
-
unset sr_ll_url sr_ll_deadline sr_ll_up
|
|
1164
|
+
unset sr_ll_url sr_ll_budget sr_ll_deadline sr_ll_up
|
|
1140
1165
|
else
|
|
1141
1166
|
sr_ll_ok="1"
|
|
1142
1167
|
fi
|
|
@@ -1419,22 +1444,37 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1419
1444
|
unset _smf _sm_model _sm_cfg _sm_attempts _SM_MAX_ATTEMPTS
|
|
1420
1445
|
fi
|
|
1421
1446
|
|
|
1422
|
-
# Configured-default LiteLLM guard
|
|
1423
|
-
#
|
|
1424
|
-
#
|
|
1425
|
-
#
|
|
1426
|
-
#
|
|
1427
|
-
#
|
|
1428
|
-
#
|
|
1429
|
-
# safe same-class fallback
|
|
1430
|
-
#
|
|
1431
|
-
#
|
|
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).
|
|
1432
1468
|
case "$_EFFECTIVE_MODEL" in
|
|
1433
1469
|
fable|claude-fable-5)
|
|
1434
1470
|
if [ -z "$_LITELLM_OK" ]; then
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
|
1438
1478
|
fi
|
|
1439
1479
|
;;
|
|
1440
1480
|
esac
|
|
@@ -1518,9 +1558,16 @@ printf '%s\n' "$_EFFECTIVE_EFFORT" > "{{agentDir}}/.active-session-effort" 2>/de
|
|
|
1518
1558
|
# /v1/models (both `fable` and `claude-fable-5` served). Other Claude sessions
|
|
1519
1559
|
# (opus/sonnet/haiku/default) keep the passthrough — it dodges the Opus SSE
|
|
1520
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).
|
|
1521
1568
|
case "$_EFFECTIVE_MODEL" in
|
|
1522
1569
|
sr-*|fable|claude-fable-5)
|
|
1523
|
-
if [ -n "$_LITELLM_OK" ] && [ -n "$SWITCHROOM_LITELLM_BASE" ]; then
|
|
1570
|
+
if { [ -n "$_LITELLM_OK" ] || [ -n "$_LITELLM_UNREACHABLE" ]; } && [ -n "$SWITCHROOM_LITELLM_BASE" ]; then
|
|
1524
1571
|
export ANTHROPIC_BASE_URL="$SWITCHROOM_LITELLM_BASE"
|
|
1525
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
|
|
1526
1573
|
fi
|
|
@@ -1531,6 +1578,46 @@ esac
|
|
|
1531
1578
|
# welcome card honest. Overwrite (not consumed) — the gateway reads it at boot.
|
|
1532
1579
|
printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.active-session-model" 2>/dev/null || true
|
|
1533
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
|
+
|
|
1534
1621
|
{{#if useSwitchroomPlugin}}
|
|
1535
1622
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
1536
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}}
|