switchroom 0.15.1 → 0.15.3
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 +1 -0
- package/dist/auth-broker/index.js +80 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +1784 -1427
- package/dist/cli/ui/index.html +67 -1
- package/dist/host-control/main.js +5 -1
- package/dist/vault/approvals/kernel-server.js +1 -0
- package/dist/vault/broker/server.js +2 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +33 -0
- package/profiles/default/CLAUDE.md.hbs +27 -0
- package/telegram-plugin/dist/gateway/gateway.js +576 -16
- package/telegram-plugin/gateway/gateway.ts +135 -4
- package/telegram-plugin/gateway/model-command.ts +368 -0
- package/telegram-plugin/tests/model-command.test.ts +349 -0
- package/telegram-plugin/uat/scenarios/jtbd-model-command-dm.test.ts +93 -0
- package/telegram-plugin/welcome-text.ts +7 -1
|
@@ -11348,6 +11348,7 @@ var AgentSchema = exports_external.object({
|
|
|
11348
11348
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
11349
11349
|
network_isolation: NetworkIsolationSchema,
|
|
11350
11350
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently — " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
11351
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet — read any agent's logs, " + "docker exec into peers, edit host files — instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent — it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
11351
11352
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only — prefer the typed fields when they exist."),
|
|
11352
11353
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
11353
11354
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
|
@@ -11348,6 +11348,7 @@ var AgentSchema = exports_external.object({
|
|
|
11348
11348
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
11349
11349
|
network_isolation: NetworkIsolationSchema,
|
|
11350
11350
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently — " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
11351
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet — read any agent's logs, " + "docker exec into peers, edit host files — instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent — it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
11351
11352
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only — prefer the typed fields when they exist."),
|
|
11352
11353
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
11353
11354
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|
|
@@ -12362,6 +12363,47 @@ function resolveConsumerProbeIntervalMs(env) {
|
|
|
12362
12363
|
return DEFAULT_CONSUMER_PROBE_INTERVAL_MS;
|
|
12363
12364
|
}
|
|
12364
12365
|
|
|
12366
|
+
// src/auth/broker/account-eligibility.ts
|
|
12367
|
+
var WALL_PCT = 99.5;
|
|
12368
|
+
var HEALTHY_CLEAR_PCT = 80;
|
|
12369
|
+
var SNAPSHOT_STALE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
12370
|
+
function snapshotFresh(s, now, maxAgeMs = SNAPSHOT_STALE_AGE_MS) {
|
|
12371
|
+
return !!s && now - s.capturedAt <= maxAgeMs && s.capturedAt <= now + 60000;
|
|
12372
|
+
}
|
|
12373
|
+
function snapshotWalled(s) {
|
|
12374
|
+
return s.fiveHourUtilizationPct >= WALL_PCT || s.sevenDayUtilizationPct >= WALL_PCT;
|
|
12375
|
+
}
|
|
12376
|
+
function snapshotClearlyHealthy(s) {
|
|
12377
|
+
return s.fiveHourUtilizationPct < HEALTHY_CLEAR_PCT && s.sevenDayUtilizationPct < HEALTHY_CLEAR_PCT;
|
|
12378
|
+
}
|
|
12379
|
+
function isAccountBlocked(opts) {
|
|
12380
|
+
const { mark, snapshot, now } = opts;
|
|
12381
|
+
if (snapshotFresh(snapshot, now)) {
|
|
12382
|
+
const markedAt = mark?.marked_at ?? 0;
|
|
12383
|
+
if (snapshot.capturedAt >= markedAt) {
|
|
12384
|
+
return snapshotWalled(snapshot);
|
|
12385
|
+
}
|
|
12386
|
+
}
|
|
12387
|
+
return mark !== undefined && mark.exhausted_until > now;
|
|
12388
|
+
}
|
|
12389
|
+
function snapshotShouldClearMark(snapshot, mark, now) {
|
|
12390
|
+
if (!mark)
|
|
12391
|
+
return false;
|
|
12392
|
+
if (!snapshotFresh(snapshot, now))
|
|
12393
|
+
return false;
|
|
12394
|
+
if (snapshot.capturedAt < (mark.marked_at ?? 0))
|
|
12395
|
+
return false;
|
|
12396
|
+
return snapshotClearlyHealthy(snapshot);
|
|
12397
|
+
}
|
|
12398
|
+
function clampMarkExpiry(opts) {
|
|
12399
|
+
const { proposedUntil, now, shortMs, snapshot } = opts;
|
|
12400
|
+
const shortCeil = now + shortMs;
|
|
12401
|
+
if (proposedUntil <= shortCeil)
|
|
12402
|
+
return proposedUntil;
|
|
12403
|
+
const liveContradictsWeeklyWall = snapshotFresh(snapshot, now) && snapshot.sevenDayUtilizationPct < WALL_PCT;
|
|
12404
|
+
return liveContradictsWeeklyWall ? shortCeil : proposedUntil;
|
|
12405
|
+
}
|
|
12406
|
+
|
|
12365
12407
|
// src/util/atomic.ts
|
|
12366
12408
|
import { randomBytes } from "node:crypto";
|
|
12367
12409
|
import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
@@ -13558,7 +13600,10 @@ function enrichMirrorContent(sourceJson) {
|
|
|
13558
13600
|
function configToShape(cfg) {
|
|
13559
13601
|
const auth = cfg.auth ?? {};
|
|
13560
13602
|
const agentsMap = cfg.agents ?? {};
|
|
13561
|
-
const adminAgents = Object.entries(agentsMap).filter(([, a]) =>
|
|
13603
|
+
const adminAgents = Object.entries(agentsMap).filter(([, a]) => {
|
|
13604
|
+
const cfg2 = a;
|
|
13605
|
+
return cfg2.admin === true || cfg2.root === true;
|
|
13606
|
+
}).map(([name]) => name);
|
|
13562
13607
|
return {
|
|
13563
13608
|
agents: Object.keys(agentsMap),
|
|
13564
13609
|
consumers: (auth.consumers ?? []).map((c) => c.name),
|
|
@@ -13774,7 +13819,8 @@ class AuthBroker {
|
|
|
13774
13819
|
}
|
|
13775
13820
|
const sockPath = this.agentSocketPath(agentName);
|
|
13776
13821
|
const uid = allocateAgentUid(agentName);
|
|
13777
|
-
const
|
|
13822
|
+
const agentCfg = this.config.agents?.[agentName];
|
|
13823
|
+
const adminFlag = agentCfg?.admin === true || agentCfg?.root === true;
|
|
13778
13824
|
await this.bindListener(sockPath, uid, 432, {
|
|
13779
13825
|
kind: "agent",
|
|
13780
13826
|
name: agentName,
|
|
@@ -14025,8 +14071,11 @@ class AuthBroker {
|
|
|
14025
14071
|
return this.accountWithFailover(account);
|
|
14026
14072
|
}
|
|
14027
14073
|
isAccountExhausted(account) {
|
|
14028
|
-
|
|
14029
|
-
|
|
14074
|
+
return isAccountBlocked({
|
|
14075
|
+
mark: this.quota[account],
|
|
14076
|
+
snapshot: this.lastQuotaCache[account],
|
|
14077
|
+
now: this.now()
|
|
14078
|
+
});
|
|
14030
14079
|
}
|
|
14031
14080
|
accountWithFailover(account) {
|
|
14032
14081
|
if (!account || !this.isAccountExhausted(account))
|
|
@@ -14062,7 +14111,7 @@ class AuthBroker {
|
|
|
14062
14111
|
const creds = readAccountCredentials(label, this.home);
|
|
14063
14112
|
const meta = readAccountMeta(label, this.home);
|
|
14064
14113
|
const q = this.quota[label];
|
|
14065
|
-
const exhausted =
|
|
14114
|
+
const exhausted = this.isAccountExhausted(label);
|
|
14066
14115
|
const lq = this.lastQuotaCache[label];
|
|
14067
14116
|
return {
|
|
14068
14117
|
label,
|
|
@@ -14137,7 +14186,7 @@ class AuthBroker {
|
|
|
14137
14186
|
cacheQuotaSnapshot(label, result) {
|
|
14138
14187
|
if (!result.ok)
|
|
14139
14188
|
return;
|
|
14140
|
-
|
|
14189
|
+
const snapshot = {
|
|
14141
14190
|
fiveHourUtilizationPct: result.data.fiveHourUtilizationPct,
|
|
14142
14191
|
sevenDayUtilizationPct: result.data.sevenDayUtilizationPct,
|
|
14143
14192
|
fiveHourResetAt: result.data.fiveHourResetAt?.toISOString() ?? null,
|
|
@@ -14147,6 +14196,13 @@ class AuthBroker {
|
|
|
14147
14196
|
overageDisabledReason: result.data.overageDisabledReason,
|
|
14148
14197
|
capturedAt: this.now()
|
|
14149
14198
|
};
|
|
14199
|
+
this.lastQuotaCache[label] = snapshot;
|
|
14200
|
+
if (snapshotShouldClearMark(snapshot, this.quota[label], this.now())) {
|
|
14201
|
+
delete this.quota[label];
|
|
14202
|
+
this.persistQuota();
|
|
14203
|
+
process.stdout.write(`auth-broker: live probe shows ${label} healthy (5h=${snapshot.fiveHourUtilizationPct}% 7d=${snapshot.sevenDayUtilizationPct}%) — cleared stale exhaustion mark
|
|
14204
|
+
`);
|
|
14205
|
+
}
|
|
14150
14206
|
}
|
|
14151
14207
|
async fleetQuotaProbeTick() {
|
|
14152
14208
|
for (const label of listAccounts(this.home)) {
|
|
@@ -14178,14 +14234,21 @@ class AuthBroker {
|
|
|
14178
14234
|
this.logErr(`consumer-quota-probe ${label}: ${err.message}`);
|
|
14179
14235
|
continue;
|
|
14180
14236
|
}
|
|
14237
|
+
this.cacheQuotaSnapshot(label, result);
|
|
14181
14238
|
const decision = quotaIndicatesExhaustion(result);
|
|
14182
14239
|
if (!decision.exhausted)
|
|
14183
14240
|
continue;
|
|
14184
|
-
const
|
|
14241
|
+
const now = this.now();
|
|
14242
|
+
const exhaustedUntil = clampMarkExpiry({
|
|
14243
|
+
proposedUntil: decision.until ?? now + MARK_EXHAUSTED_DEFAULT_MS,
|
|
14244
|
+
now,
|
|
14245
|
+
shortMs: MARK_EXHAUSTED_DEFAULT_MS,
|
|
14246
|
+
snapshot: this.lastQuotaCache[label]
|
|
14247
|
+
});
|
|
14185
14248
|
const existing = this.quota[label]?.exhausted_until;
|
|
14186
14249
|
if (existing !== undefined && existing >= exhaustedUntil)
|
|
14187
14250
|
continue;
|
|
14188
|
-
this.quota[label] = { exhausted_until: exhaustedUntil };
|
|
14251
|
+
this.quota[label] = { exhausted_until: exhaustedUntil, marked_at: now };
|
|
14189
14252
|
this.persistQuota();
|
|
14190
14253
|
this.audit({ op: "mark-exhausted", identity: { kind: "operator" }, account: label, ok: true });
|
|
14191
14254
|
process.stdout.write(`auth-broker: consumer-quota-sensor marked ${label} exhausted until ${new Date(exhaustedUntil).toISOString()} — consumer(s) fail over
|
|
@@ -14219,8 +14282,14 @@ class AuthBroker {
|
|
|
14219
14282
|
socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", "no active account configured"));
|
|
14220
14283
|
return;
|
|
14221
14284
|
}
|
|
14222
|
-
const
|
|
14223
|
-
|
|
14285
|
+
const now = this.now();
|
|
14286
|
+
const exhaustedUntil = clampMarkExpiry({
|
|
14287
|
+
proposedUntil: until ?? now + MARK_EXHAUSTED_DEFAULT_MS,
|
|
14288
|
+
now,
|
|
14289
|
+
shortMs: MARK_EXHAUSTED_DEFAULT_MS,
|
|
14290
|
+
snapshot: this.lastQuotaCache[account]
|
|
14291
|
+
});
|
|
14292
|
+
this.quota[account] = { exhausted_until: exhaustedUntil, marked_at: now };
|
|
14224
14293
|
this.persistQuota();
|
|
14225
14294
|
const rolled = this.fanoutFailoverFor(account);
|
|
14226
14295
|
const rolledTo = this.nextHealthyAccount(account, this.config.auth?.fallback_order ?? []);
|
|
@@ -14747,9 +14816,7 @@ class AuthBroker {
|
|
|
14747
14816
|
const cand = order[(start + i) % order.length];
|
|
14748
14817
|
if (!cand)
|
|
14749
14818
|
continue;
|
|
14750
|
-
|
|
14751
|
-
const exhausted = q !== undefined && q.exhausted_until > this.now();
|
|
14752
|
-
if (!exhausted && accountExists(cand, this.home))
|
|
14819
|
+
if (!this.isAccountExhausted(cand) && accountExists(cand, this.home))
|
|
14753
14820
|
return cand;
|
|
14754
14821
|
}
|
|
14755
14822
|
return null;
|
|
@@ -12096,6 +12096,7 @@ var AgentSchema = exports_external.object({
|
|
|
12096
12096
|
dangerous_mode: exports_external.boolean().optional().describe("If true, include --dangerously-skip-permissions in start.sh"),
|
|
12097
12097
|
network_isolation: NetworkIsolationSchema,
|
|
12098
12098
|
admin: exports_external.boolean().optional().describe("If true, the agent's Telegram gateway intercepts admin slash commands " + "(/agents, /logs, /restart, /delete, /update, /auth, /reconcile, etc.) " + "locally before forwarding to Claude. Commands are handled silently \u2014 " + "Claude never sees them. Requires the agent to use the switchroom-telegram " + "plugin. When false or absent, all messages pass through to Claude unchanged."),
|
|
12099
|
+
root: exports_external.boolean().optional().describe("If true, this is a ROOT-tier debugging agent: a root-privileged " + "container (runs as uid 0, mounts /var/run/docker.sock, the whole " + "~/.switchroom tree, and the host root filesystem at /host) so you " + "can DM it to debug the whole fleet \u2014 read any agent's logs, " + "docker exec into peers, edit host files \u2014 instead of SSHing into " + "the host as root. Implies admin: true (all admin slash commands). " + "Standing root power, audited via the agent's own session transcript " + "and shell history; there is no per-action approval tap. Per-agent " + "only (never set at defaults/profile layers). Grant to exactly one " + "trusted operator-private agent \u2014 it ingests other agents' output, " + "which is attacker-influenced text. See docs/root-agent.md."),
|
|
12099
12100
|
settings_raw: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Escape hatch: raw object deep-merged into the generated " + "settings.json as the final step. Use for Claude Code settings " + "keys switchroom doesn't wrap directly (e.g. effort, apiKeyHelper). " + "Power-user-only \u2014 prefer the typed fields when they exist."),
|
|
12100
12101
|
claude_md_raw: exports_external.string().optional().describe("Escape hatch: markdown text appended verbatim to CLAUDE.md on " + "initial scaffold. Not re-applied on reconcile (CLAUDE.md is " + "user-protected). Use for one-off persona tuning that isn't " + "worth a template."),
|
|
12101
12102
|
cli_args: exports_external.array(exports_external.string()).optional().describe("Escape hatch: extra arguments appended to the `exec claude` " + "invocation in start.sh. Use for Claude Code CLI flags switchroom " + "doesn't expose directly (e.g. --effort high, " + "--exclude-dynamic-system-prompt-sections)."),
|