switchroom 0.19.45 → 0.19.46
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/cli/switchroom.js +100 -12
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +48 -4
- package/telegram-plugin/dist/gateway/gateway.js +17 -12
- package/telegram-plugin/gateway/auth-command.ts +7 -4
- package/telegram-plugin/gateway/auth-loopback-relay.ts +39 -20
- package/telegram-plugin/gateway/gateway.ts +1 -1
- package/telegram-plugin/tests/auth-loopback-relay.test.ts +58 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.19.
|
|
2123
|
+
var VERSION = "0.19.46", COMMIT_SHA = "306c3ed1";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -37439,11 +37439,14 @@ __export(exports_drive, {
|
|
|
37439
37439
|
selectGoogleWorkspaceScopes: () => selectGoogleWorkspaceScopes,
|
|
37440
37440
|
selectDriveAccountScopes: () => selectDriveAccountScopes,
|
|
37441
37441
|
runDriveOAuthFlow: () => runDriveOAuthFlow,
|
|
37442
|
+
resolveReconsentCapabilities: () => resolveReconsentCapabilities,
|
|
37442
37443
|
registerDriveCommand: () => registerDriveCommand,
|
|
37444
|
+
capabilitiesFromScopeString: () => capabilitiesFromScopeString,
|
|
37443
37445
|
__test: () => __test,
|
|
37444
37446
|
GOOGLE_SLIDES_SCOPE: () => GOOGLE_SLIDES_SCOPE,
|
|
37445
37447
|
GOOGLE_SHEETS_SCOPE: () => GOOGLE_SHEETS_SCOPE,
|
|
37446
37448
|
GOOGLE_DOCS_SCOPE: () => GOOGLE_DOCS_SCOPE,
|
|
37449
|
+
GOOGLE_CALENDAR_READONLY_SCOPE: () => GOOGLE_CALENDAR_READONLY_SCOPE,
|
|
37447
37450
|
DRIVE_WRITE_SCOPES: () => DRIVE_WRITE_SCOPES,
|
|
37448
37451
|
DRIVE_READONLY_SCOPES: () => DRIVE_READONLY_SCOPES
|
|
37449
37452
|
});
|
|
@@ -37461,7 +37464,29 @@ function selectDriveAccountScopes(write) {
|
|
|
37461
37464
|
function selectGoogleWorkspaceScopes(opts) {
|
|
37462
37465
|
const base = selectDriveAccountScopes(opts.write);
|
|
37463
37466
|
const workspace = workspaceScopesForTier(opts.tier ?? "core");
|
|
37464
|
-
|
|
37467
|
+
const calendar = opts.calendar ? [GOOGLE_CALENDAR_READONLY_SCOPE] : [];
|
|
37468
|
+
return [...new Set([...base, ...workspace, ...calendar])];
|
|
37469
|
+
}
|
|
37470
|
+
function capabilitiesFromScopeString(scope) {
|
|
37471
|
+
const have = new Set(scope.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0));
|
|
37472
|
+
return {
|
|
37473
|
+
write: have.has("https://www.googleapis.com/auth/drive.file"),
|
|
37474
|
+
calendar: have.has(GOOGLE_CALENDAR_READONLY_SCOPE)
|
|
37475
|
+
};
|
|
37476
|
+
}
|
|
37477
|
+
function resolveReconsentCapabilities(requested, existing) {
|
|
37478
|
+
const keys = ["write", "calendar"];
|
|
37479
|
+
if (!existing) {
|
|
37480
|
+
return { effective: { ...requested }, carried: [], added: [] };
|
|
37481
|
+
}
|
|
37482
|
+
return {
|
|
37483
|
+
effective: {
|
|
37484
|
+
write: requested.write || existing.write,
|
|
37485
|
+
calendar: requested.calendar || existing.calendar
|
|
37486
|
+
},
|
|
37487
|
+
carried: keys.filter((k) => existing[k] && !requested[k]),
|
|
37488
|
+
added: keys.filter((k) => requested[k] && !existing[k])
|
|
37489
|
+
};
|
|
37465
37490
|
}
|
|
37466
37491
|
function getVaultPath(configPath) {
|
|
37467
37492
|
try {
|
|
@@ -37875,7 +37900,7 @@ function registerDriveCommand(program3, deps = {}) {
|
|
|
37875
37900
|
await runDisconnect({ agentName: agent }, deps);
|
|
37876
37901
|
});
|
|
37877
37902
|
}
|
|
37878
|
-
var EXIT_OK = 0, EXIT_DENIED = 1, EXIT_TIMEOUT = 2, EXIT_RATE_LIMITED = 3, EXIT_ERROR = 4, EXIT_ABORTED = 130, DRIVE_READONLY_SCOPES, DRIVE_WRITE_SCOPES, DEFAULT_SCOPES, GOOGLE_DOCS_SCOPE = "https://www.googleapis.com/auth/documents", GOOGLE_SHEETS_SCOPE = "https://www.googleapis.com/auth/spreadsheets", GOOGLE_SLIDES_SCOPE = "https://www.googleapis.com/auth/presentations", __test;
|
|
37903
|
+
var EXIT_OK = 0, EXIT_DENIED = 1, EXIT_TIMEOUT = 2, EXIT_RATE_LIMITED = 3, EXIT_ERROR = 4, EXIT_ABORTED = 130, DRIVE_READONLY_SCOPES, DRIVE_WRITE_SCOPES, DEFAULT_SCOPES, GOOGLE_DOCS_SCOPE = "https://www.googleapis.com/auth/documents", GOOGLE_SHEETS_SCOPE = "https://www.googleapis.com/auth/spreadsheets", GOOGLE_SLIDES_SCOPE = "https://www.googleapis.com/auth/presentations", GOOGLE_CALENDAR_READONLY_SCOPE = "https://www.googleapis.com/auth/calendar.readonly", __test;
|
|
37879
37904
|
var init_drive = __esm(() => {
|
|
37880
37905
|
init_source();
|
|
37881
37906
|
init_loader();
|
|
@@ -77304,10 +77329,15 @@ function pad(s, width) {
|
|
|
77304
77329
|
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
77305
77330
|
}
|
|
77306
77331
|
function registerAccountAdd(accountParent) {
|
|
77307
|
-
accountParent.command("add <account>").description("Mint a Google OAuth refresh token for <account> and register it with the auth-broker. For Drive scopes the effective flow is desktop-loopback (device-code returns invalid_scope for Drive; OOB is retired) \u2014 use a Desktop OAuth client; on a headless host complete the browser step over an SSH port-forward. Add --write for create/edit (drive.file); default is read-only.").option("--replace", "Overwrite existing credentials for <account> (default refuses if account already registered)", false).option("--write", "Request Drive WRITE scope (drive.file: create + edit app-created files) in addition to read. Default is read-only \u2014 a read grant never silently becomes a write grant. Re-consent an existing account with `--replace --write`.", false).action(withConfigError(async (account, opts) => {
|
|
77332
|
+
accountParent.command("add <account>").description("Mint a Google OAuth refresh token for <account> and register it with the auth-broker. For Drive scopes the effective flow is desktop-loopback (device-code returns invalid_scope for Drive; OOB is retired) \u2014 use a Desktop OAuth client; on a headless host complete the browser step over an SSH port-forward. Add --write for create/edit (drive.file) and --calendar for read-only Calendar; default is Drive read-only.").option("--replace", "Overwrite existing credentials for <account> (default refuses if account already registered)", false).option("--write", "Request Drive WRITE scope (drive.file: create + edit app-created files) in addition to read. Default is read-only \u2014 a read grant never silently becomes a write grant. Re-consent an existing account with `--replace --write`.", false).option("--calendar", "Request READ-ONLY Calendar scope (calendar.readonly) so the gdrive MCP's list_calendars / get_events tools authenticate. Opt-in \u2014 no tier grants it by default, and switchroom never requests a Calendar WRITE scope. Re-consent an existing account with `--replace --calendar`.", false).action(withConfigError(async (account, opts) => {
|
|
77308
77333
|
const normalizedAccount = validateAndNormalizeAccountEmail(account);
|
|
77309
77334
|
const [
|
|
77310
|
-
{
|
|
77335
|
+
{
|
|
77336
|
+
runDriveOAuthFlow: runDriveOAuthFlow2,
|
|
77337
|
+
selectGoogleWorkspaceScopes: selectGoogleWorkspaceScopes2,
|
|
77338
|
+
capabilitiesFromScopeString: capabilitiesFromScopeString2,
|
|
77339
|
+
resolveReconsentCapabilities: resolveReconsentCapabilities2
|
|
77340
|
+
},
|
|
77311
77341
|
{ selectInitialTier: selectInitialTier2 },
|
|
77312
77342
|
{ brokerCall: brokerCall2 },
|
|
77313
77343
|
{ loadConfig: loadConfig2, resolvePath: resolvePath2 },
|
|
@@ -77377,8 +77407,14 @@ function registerAccountAdd(accountParent) {
|
|
|
77377
77407
|
clientSecretRaw = await resolveRef(clientSecretRaw, "google_client_secret");
|
|
77378
77408
|
}
|
|
77379
77409
|
const tier = gw.tier ?? "core";
|
|
77410
|
+
const existingEntry = (await brokerCall2(async (client) => client.listGoogleAccounts())).accounts.find((a) => a.account === normalizedAccount);
|
|
77411
|
+
const plan = resolveReconsentCapabilities2({ write: opts.write ?? false, calendar: opts.calendar ?? false }, existingEntry ? capabilitiesFromScopeString2(existingEntry.scope) : undefined);
|
|
77412
|
+
if (existingEntry && !opts.replace && plan.added.length > 0) {
|
|
77413
|
+
throw new Error(buildScopeReconsentRequiredError(normalizedAccount, plan.added));
|
|
77414
|
+
}
|
|
77380
77415
|
const accountScopes = selectGoogleWorkspaceScopes2({
|
|
77381
|
-
write:
|
|
77416
|
+
write: plan.effective.write,
|
|
77417
|
+
calendar: plan.effective.calendar,
|
|
77382
77418
|
tier
|
|
77383
77419
|
});
|
|
77384
77420
|
const oauthCfg = {
|
|
@@ -77386,9 +77422,15 @@ function registerAccountAdd(accountParent) {
|
|
|
77386
77422
|
client_secret: clientSecretRaw,
|
|
77387
77423
|
scopes: accountScopes
|
|
77388
77424
|
};
|
|
77389
|
-
if (
|
|
77425
|
+
if (plan.effective.write) {
|
|
77390
77426
|
console.log(source_default.yellow(" Requesting Drive WRITE scope (drive.file \u2014 create/edit app-created files)."));
|
|
77391
77427
|
}
|
|
77428
|
+
if (plan.effective.calendar) {
|
|
77429
|
+
console.log(source_default.yellow(" Requesting Calendar READ-ONLY scope (calendar.readonly \u2014 list calendars, read events)."));
|
|
77430
|
+
}
|
|
77431
|
+
for (const line of buildCarryForwardNotices(plan.carried)) {
|
|
77432
|
+
console.log(source_default.gray(line));
|
|
77433
|
+
}
|
|
77392
77434
|
console.log(source_default.gray(` Workspace tier: ${tier} \u2014 requesting Docs + Sheets` + (tier === "extended" || tier === "complete" ? " + Slides" : "") + " API scopes so the tier's tools can authenticate."));
|
|
77393
77435
|
console.log(source_default.gray(` Changing the tier later requires re-running this command
|
|
77394
77436
|
` + " (`--replace`) \u2014 OAuth scopes are fixed at consent time."));
|
|
@@ -77425,6 +77467,40 @@ function registerAccountAdd(accountParent) {
|
|
|
77425
77467
|
console.log();
|
|
77426
77468
|
}));
|
|
77427
77469
|
}
|
|
77470
|
+
var CAPABILITY_LABELS = {
|
|
77471
|
+
write: {
|
|
77472
|
+
flag: "--write",
|
|
77473
|
+
scope: "drive.file",
|
|
77474
|
+
summary: "Drive write (create + edit app-created files)"
|
|
77475
|
+
},
|
|
77476
|
+
calendar: {
|
|
77477
|
+
flag: "--calendar",
|
|
77478
|
+
scope: "calendar.readonly",
|
|
77479
|
+
summary: "Calendar read-only (list calendars, read events)"
|
|
77480
|
+
}
|
|
77481
|
+
};
|
|
77482
|
+
function buildScopeReconsentRequiredError(account, added) {
|
|
77483
|
+
const flags = added.map((k) => CAPABILITY_LABELS[k].flag);
|
|
77484
|
+
const scopes = added.map((k) => CAPABILITY_LABELS[k].scope).join(", ");
|
|
77485
|
+
return [
|
|
77486
|
+
`'${account}' is already registered with the auth-broker, and its stored ` + `token does not carry ${scopes}.`,
|
|
77487
|
+
"",
|
|
77488
|
+
"OAuth scopes are fixed at consent time \u2014 a stored token cannot be",
|
|
77489
|
+
"widened in place. Re-consent to mint a new token that includes it:",
|
|
77490
|
+
"",
|
|
77491
|
+
` switchroom auth google account add ${account} --replace ${flags.join(" ")}`,
|
|
77492
|
+
"",
|
|
77493
|
+
"Scopes the account already holds are carried forward automatically,",
|
|
77494
|
+
"so re-consenting will not drop existing capabilities."
|
|
77495
|
+
].join(`
|
|
77496
|
+
`);
|
|
77497
|
+
}
|
|
77498
|
+
function buildCarryForwardNotices(carried) {
|
|
77499
|
+
return carried.map((k) => {
|
|
77500
|
+
const { flag, scope, summary } = CAPABILITY_LABELS[k];
|
|
77501
|
+
return ` Carrying forward ${summary} \u2014 the existing token holds ${scope}, ` + `so it is re-requested even though ${flag} was not passed ` + `(re-consent would otherwise silently revoke it).`;
|
|
77502
|
+
});
|
|
77503
|
+
}
|
|
77428
77504
|
function oauthClientSetupGuidance(reason) {
|
|
77429
77505
|
return [
|
|
77430
77506
|
reason,
|
|
@@ -100174,12 +100250,25 @@ function findMissingWorkspaceScopes(seedScope, tier) {
|
|
|
100174
100250
|
return requiredWorkspaceScopesForTier(tier).filter((s) => !have.has(s));
|
|
100175
100251
|
}
|
|
100176
100252
|
var DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
100177
|
-
|
|
100253
|
+
var CALENDAR_READONLY_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
|
|
100254
|
+
function seedOptInCapabilities(seedScope) {
|
|
100255
|
+
const have = new Set(seedScope.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0));
|
|
100256
|
+
return {
|
|
100257
|
+
write: have.has(DRIVE_FILE_SCOPE),
|
|
100258
|
+
calendar: have.has(CALENDAR_READONLY_SCOPE)
|
|
100259
|
+
};
|
|
100260
|
+
}
|
|
100261
|
+
function buildMissingScopeWarning(missing, tier, accountEmail, granted) {
|
|
100178
100262
|
const short = missing.map((s) => s.replace(/^https:\/\/www\.googleapis\.com\/auth\//, "")).join(", ");
|
|
100263
|
+
const suffix = (granted.write ? " --write" : "") + (granted.calendar ? " --calendar" : "");
|
|
100264
|
+
const preserved = [
|
|
100265
|
+
granted.write ? "--write preserves the existing Drive write capability" : "",
|
|
100266
|
+
granted.calendar ? "--calendar preserves the existing Calendar read capability" : ""
|
|
100267
|
+
].filter((s) => s.length > 0);
|
|
100179
100268
|
return `drive-mcp-launcher: WARNING \u2014 the Google account '${accountEmail}' was ` + `consented WITHOUT the scope(s) needed for tier '${tier ?? "core"}': ` + `${short}.
|
|
100180
100269
|
` + ` The matching MCP tools (Docs / Sheets / Slides create+edit) will FAIL ` + `to authenticate. OAuth scopes are fixed at consent time \u2014 re-run on the ` + `host to re-mint the token with the correct scopes:
|
|
100181
|
-
` + ` switchroom auth google account add ${accountEmail} --replace` + `${
|
|
100182
|
-
` + ` (scopes are derived from \`google_workspace.tier\` \u2014 set the tier ` + `before re-running${
|
|
100270
|
+
` + ` switchroom auth google account add ${accountEmail} --replace` + `${suffix}
|
|
100271
|
+
` + ` (scopes are derived from \`google_workspace.tier\` \u2014 set the tier ` + `before re-running${preserved.length > 0 ? "; " + preserved.join("; ") : ""}` + `). Drive read/file tools are unaffected.
|
|
100183
100272
|
`;
|
|
100184
100273
|
}
|
|
100185
100274
|
function buildUvxArgs(tier) {
|
|
@@ -100427,8 +100516,7 @@ async function runDriveMcpLauncher(opts) {
|
|
|
100427
100516
|
const tier = opts.tier ?? configSecrets.tier;
|
|
100428
100517
|
const missingScopes = findMissingWorkspaceScopes(brokerCreds.scope, tier);
|
|
100429
100518
|
if (missingScopes.length > 0) {
|
|
100430
|
-
|
|
100431
|
-
process.stderr.write(buildMissingScopeWarning(missingScopes, tier, brokerCreds.accountEmail, hasWriteScope));
|
|
100519
|
+
process.stderr.write(buildMissingScopeWarning(missingScopes, tier, brokerCreds.accountEmail, seedOptInCapabilities(brokerCreds.scope)));
|
|
100432
100520
|
}
|
|
100433
100521
|
const args = buildUvxArgs(tier);
|
|
100434
100522
|
const env2 = buildChildEnv(process.env, credentialsDir, brokerCreds.accountEmail);
|
|
@@ -21402,7 +21402,7 @@ function allocateAgentUid(name) {
|
|
|
21402
21402
|
}
|
|
21403
21403
|
|
|
21404
21404
|
// src/build-info.ts
|
|
21405
|
-
var VERSION = "0.19.
|
|
21405
|
+
var VERSION = "0.19.46";
|
|
21406
21406
|
|
|
21407
21407
|
// src/setup/hindsight-recall-tunables.ts
|
|
21408
21408
|
var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
|
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.19.
|
|
4
|
+
"version": "0.19.46",
|
|
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": {
|
|
@@ -805,7 +805,26 @@ export SWITCHROOM_AGENT_START_CWD="{{agentDir}}"
|
|
|
805
805
|
# already gets this via .mcp.json env, but the Claude Code process
|
|
806
806
|
# doesn't inherit that — so without this export, `switchroom auth status`
|
|
807
807
|
# from Bash inside the agent fails with "No switchroom.yaml found".
|
|
808
|
-
|
|
808
|
+
#
|
|
809
|
+
# DEFER to a pre-set value: in Docker, compose.ts already sets
|
|
810
|
+
# SWITCHROOM_CONFIG to the in-container mount (/state/config/
|
|
811
|
+
# switchroom.yaml). The value baked here is the HOST path — resolve()d on
|
|
812
|
+
# the machine that ran the scaffold — which does not exist inside the
|
|
813
|
+
# container. Unconditionally exporting it clobbered the correct compose
|
|
814
|
+
# value and silently killed the live model/effort resolvers below (their
|
|
815
|
+
# [ -f "$SWITCHROOM_CONFIG" ] guard failed and fell into the quiet
|
|
816
|
+
# no-config-mount branch), so `model:` edits + bare restart did nothing.
|
|
817
|
+
# The bake stays as the fallback for host-run (non-Docker) agents, where
|
|
818
|
+
# nothing else sets the var. ${VAR:-} also treats a set-but-EMPTY var as
|
|
819
|
+
# unset, so an empty compose env can never blank the fallback.
|
|
820
|
+
#
|
|
821
|
+
# NB the baked path is pre-single-quoted by the scaffold, so it is
|
|
822
|
+
# assigned BARE to a temp var first — embedding it directly inside
|
|
823
|
+
# "${SWITCHROOM_CONFIG:-...}" would keep the literal quote characters in
|
|
824
|
+
# the value.
|
|
825
|
+
_SR_CONFIG_BAKED={{{switchroomConfigPathQ}}}
|
|
826
|
+
export SWITCHROOM_CONFIG="${SWITCHROOM_CONFIG:-$_SR_CONFIG_BAKED}"
|
|
827
|
+
unset _SR_CONFIG_BAKED
|
|
809
828
|
{{/if}}
|
|
810
829
|
{{#if hindsightEnabled}}
|
|
811
830
|
# Hindsight memory plugin (vendored at .claude/plugins/hindsight-memory/).
|
|
@@ -1722,9 +1741,11 @@ fi
|
|
|
1722
1741
|
# launcher recorded on every prior boot (read BEFORE it is overwritten);
|
|
1723
1742
|
# 3. the apply-time bake.
|
|
1724
1743
|
# Steps 2/3 append to `.session-model-alert` so the gateway tells the
|
|
1725
|
-
# operator once at boot. No SWITCHROOM_CONFIG
|
|
1726
|
-
# nothing to read live — the bake IS the truth there
|
|
1727
|
-
#
|
|
1744
|
+
# operator once at boot. No SWITCHROOM_CONFIG at all means there is
|
|
1745
|
+
# nothing to read live — the bake IS the truth there and the fallback is
|
|
1746
|
+
# silent. But SWITCHROOM_CONFIG set with the FILE MISSING is a
|
|
1747
|
+
# misconfiguration that kills live resolution outright, so that case is
|
|
1748
|
+
# loud (stderr + operator alert) — see the else branch below.
|
|
1728
1749
|
#
|
|
1729
1750
|
# The yaml file bind mount re-resolves by PATH at container start, so a
|
|
1730
1751
|
# host-side atomic-rename write is always picked up by the NEXT boot — the
|
|
@@ -1785,6 +1806,23 @@ if [ -n "$SWITCHROOM_CONFIG" ] && [ -f "$SWITCHROOM_CONFIG" ] && command -v swit
|
|
|
1785
1806
|
unset _lm_live _lm_try _lm_baked_class _lm_live_class
|
|
1786
1807
|
else
|
|
1787
1808
|
_EFFECTIVE_MODEL="$_BAKED_MODEL"
|
|
1809
|
+
# No live read possible. Distinguish the legitimately quiet case — no
|
|
1810
|
+
# SWITCHROOM_CONFIG at all (host-run agent scaffolded without a config
|
|
1811
|
+
# path; the bake IS the truth) — from a broken resolver hiding behind
|
|
1812
|
+
# it. SWITCHROOM_CONFIG set but pointing at a file that does not exist
|
|
1813
|
+
# is a misconfiguration (e.g. a host path leaking into a container env)
|
|
1814
|
+
# that silently disables live `model:`/`thinking_effort:` resolution:
|
|
1815
|
+
# exactly the defect that let the start.sh host-path clobber of the
|
|
1816
|
+
# compose-set env go unnoticed. That case is LOUD — stderr + a one-boot
|
|
1817
|
+
# operator alert. A set config whose file exists but with no
|
|
1818
|
+
# `switchroom` CLI on PATH is equally dead, stderr-only (nothing the
|
|
1819
|
+
# operator can act on from chat).
|
|
1820
|
+
if [ -n "$SWITCHROOM_CONFIG" ] && [ ! -f "$SWITCHROOM_CONFIG" ]; then
|
|
1821
|
+
echo "session-model: SWITCHROOM_CONFIG='$SWITCHROOM_CONFIG' is set but the file does not exist — live config resolution is DISABLED; using apply-time baked model '$_BAKED_MODEL'" >&2
|
|
1822
|
+
printf '`SWITCHROOM_CONFIG` points at `%s`, which does not exist in this environment, so live `model:`/`thinking_effort:` resolution is disabled and the agent booted on the apply-time default `%s`. Edits to switchroom.yaml will NOT take effect on a bare restart until this is fixed.\n' "$SWITCHROOM_CONFIG" "$_BAKED_MODEL" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1823
|
+
elif [ -n "$SWITCHROOM_CONFIG" ]; then
|
|
1824
|
+
echo "session-model: SWITCHROOM_CONFIG is set but no 'switchroom' CLI is on PATH — live config resolution is DISABLED; using apply-time baked model '$_BAKED_MODEL'" >&2
|
|
1825
|
+
fi
|
|
1788
1826
|
fi
|
|
1789
1827
|
unset _BAKED_MODEL _LKG_MODEL
|
|
1790
1828
|
# Record the RESOLVED configured default (raw, unquoted) every boot, before
|
|
@@ -2078,6 +2116,12 @@ if [ -n "$SWITCHROOM_CONFIG" ] && [ -f "$SWITCHROOM_CONFIG" ] && command -v swit
|
|
|
2078
2116
|
unset _le_live _le_try
|
|
2079
2117
|
else
|
|
2080
2118
|
_EFFECTIVE_EFFORT="$_BAKED_EFFORT"
|
|
2119
|
+
# Misconfig visibility, parity with the model sibling above — which
|
|
2120
|
+
# owns the operator alert for this shared root cause; a second alert
|
|
2121
|
+
# here would double-post for the same broken env. stderr only.
|
|
2122
|
+
if [ -n "$SWITCHROOM_CONFIG" ] && [ ! -f "$SWITCHROOM_CONFIG" ]; then
|
|
2123
|
+
echo "session-effort: SWITCHROOM_CONFIG='$SWITCHROOM_CONFIG' is set but the file does not exist — live config resolution is DISABLED; using apply-time baked thinking_effort '${_BAKED_EFFORT:-<unset>}'" >&2
|
|
2124
|
+
fi
|
|
2081
2125
|
fi
|
|
2082
2126
|
unset _BAKED_EFFORT _LKG_EFFORT
|
|
2083
2127
|
# Record the LIVE-RESOLVED configured default, BEFORE the carrier below can
|
|
@@ -72781,7 +72781,7 @@ function parseProviderVerb(provider, rest) {
|
|
|
72781
72781
|
return { kind: "provider-cancel", provider };
|
|
72782
72782
|
}
|
|
72783
72783
|
if (sub !== "add") {
|
|
72784
|
-
const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
|
|
72784
|
+
const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write] [--calendar]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
|
|
72785
72785
|
return {
|
|
72786
72786
|
kind: "help",
|
|
72787
72787
|
reason: `Unknown \`${provider}\` subcommand: \`${codeSpanSafe(sub || "(none)")}\`. ${usage}`
|
|
@@ -72792,13 +72792,13 @@ function parseProviderVerb(provider, rest) {
|
|
|
72792
72792
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
72793
72793
|
const email = positional[0];
|
|
72794
72794
|
if (!email) {
|
|
72795
|
-
const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
|
|
72795
|
+
const usage = provider === "google" ? "Usage: /auth google add <email> [--replace] [--write] [--calendar]" : "Usage: /auth microsoft add <email> [--replace] [--org-mode]";
|
|
72796
72796
|
return { kind: "help", reason: usage };
|
|
72797
72797
|
}
|
|
72798
72798
|
const emailErr = validateAuthAddLabel(email);
|
|
72799
72799
|
if (emailErr)
|
|
72800
72800
|
return { kind: "help", reason: emailErr };
|
|
72801
|
-
const allowed = provider === "google" ? new Set(["--replace", "--write"]) : new Set(["--replace", "--org-mode"]);
|
|
72801
|
+
const allowed = provider === "google" ? new Set(["--replace", "--write", "--calendar"]) : new Set(["--replace", "--org-mode"]);
|
|
72802
72802
|
for (const f of flags) {
|
|
72803
72803
|
if (!allowed.has(f)) {
|
|
72804
72804
|
return {
|
|
@@ -72813,6 +72813,7 @@ function parseProviderVerb(provider, rest) {
|
|
|
72813
72813
|
email,
|
|
72814
72814
|
replace: flags.has("--replace"),
|
|
72815
72815
|
write: provider === "google" && flags.has("--write"),
|
|
72816
|
+
calendar: provider === "google" && flags.has("--calendar"),
|
|
72816
72817
|
orgMode: provider === "microsoft" && flags.has("--org-mode")
|
|
72817
72818
|
};
|
|
72818
72819
|
}
|
|
@@ -74520,16 +74521,16 @@ ${loginUrl}
|
|
|
74520
74521
|
|
|
74521
74522
|
// gateway/auth-loopback-relay.ts
|
|
74522
74523
|
import { spawn } from "node:child_process";
|
|
74523
|
-
function
|
|
74524
|
-
|
|
74525
|
-
const args = provider === "google" ? [
|
|
74524
|
+
function buildRelayArgs(provider, email, opts) {
|
|
74525
|
+
return provider === "google" ? [
|
|
74526
74526
|
"auth",
|
|
74527
74527
|
"google",
|
|
74528
74528
|
"account",
|
|
74529
74529
|
"add",
|
|
74530
74530
|
email,
|
|
74531
74531
|
...opts.replace ? ["--replace"] : [],
|
|
74532
|
-
...opts.write ? ["--write"] : []
|
|
74532
|
+
...opts.write ? ["--write"] : [],
|
|
74533
|
+
...opts.calendar ? ["--calendar"] : []
|
|
74533
74534
|
] : [
|
|
74534
74535
|
"auth",
|
|
74535
74536
|
"microsoft",
|
|
@@ -74539,6 +74540,10 @@ function defaultSpawnRelay(provider, email, opts) {
|
|
|
74539
74540
|
...opts.replace ? ["--replace"] : [],
|
|
74540
74541
|
...opts.orgMode ? ["--org-mode"] : []
|
|
74541
74542
|
];
|
|
74543
|
+
}
|
|
74544
|
+
function defaultSpawnRelay(provider, email, opts) {
|
|
74545
|
+
const binary = opts.binary ?? "switchroom";
|
|
74546
|
+
const args = buildRelayArgs(provider, email, opts);
|
|
74542
74547
|
const child = spawn(binary, args, {
|
|
74543
74548
|
stdio: ["ignore", "pipe", "pipe"],
|
|
74544
74549
|
env: {
|
|
@@ -101041,10 +101046,10 @@ function startOutboxSweep(deps) {
|
|
|
101041
101046
|
}
|
|
101042
101047
|
|
|
101043
101048
|
// ../src/build-info.ts
|
|
101044
|
-
var VERSION2 = "0.19.
|
|
101045
|
-
var COMMIT_SHA = "
|
|
101046
|
-
var COMMIT_DATE = "2026-08-
|
|
101047
|
-
var LATEST_PR =
|
|
101049
|
+
var VERSION2 = "0.19.46";
|
|
101050
|
+
var COMMIT_SHA = "306c3ed1";
|
|
101051
|
+
var COMMIT_DATE = "2026-08-02T00:29:39Z";
|
|
101052
|
+
var LATEST_PR = 4194;
|
|
101048
101053
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
101049
101054
|
|
|
101050
101055
|
// gateway/boot-version.ts
|
|
@@ -112527,7 +112532,7 @@ Set \`admin: true\` on this agent in switchroom.yaml to unlock.`, { html: true }
|
|
|
112527
112532
|
return;
|
|
112528
112533
|
}
|
|
112529
112534
|
try {
|
|
112530
|
-
const { consentUrl, state: state7, port, child } = await startLoopbackFlow(parsed.provider, parsed.email, { replace: parsed.replace, write: parsed.write, orgMode: parsed.orgMode });
|
|
112535
|
+
const { consentUrl, state: state7, port, child } = await startLoopbackFlow(parsed.provider, parsed.email, { replace: parsed.replace, write: parsed.write, calendar: parsed.calendar, orgMode: parsed.orgMode });
|
|
112531
112536
|
const newFlow = {
|
|
112532
112537
|
provider: parsed.provider,
|
|
112533
112538
|
email: parsed.email,
|
|
@@ -65,6 +65,8 @@ export type ParsedAuthCommand =
|
|
|
65
65
|
replace: boolean
|
|
66
66
|
/** Google `--write` (Drive write scope). */
|
|
67
67
|
write: boolean
|
|
68
|
+
/** Google `--calendar` (read-only Calendar scope). */
|
|
69
|
+
calendar: boolean
|
|
68
70
|
/** Microsoft `--org-mode`. */
|
|
69
71
|
orgMode: boolean
|
|
70
72
|
}
|
|
@@ -251,7 +253,7 @@ export function parseAuthCommand(text: string): ParsedAuthCommand | null {
|
|
|
251
253
|
* wired; anything else is a help-with-reason so the operator sees the shape.
|
|
252
254
|
*
|
|
253
255
|
* Flags mirror the CLI:
|
|
254
|
-
* google: `add <email> [--replace] [--write]`
|
|
256
|
+
* google: `add <email> [--replace] [--write] [--calendar]`
|
|
255
257
|
* microsoft: `add <email> [--replace] [--org-mode]`
|
|
256
258
|
*/
|
|
257
259
|
export function parseProviderVerb(
|
|
@@ -265,7 +267,7 @@ export function parseProviderVerb(
|
|
|
265
267
|
if (sub !== 'add') {
|
|
266
268
|
const usage =
|
|
267
269
|
provider === 'google'
|
|
268
|
-
? 'Usage: /auth google add <email> [--replace] [--write]'
|
|
270
|
+
? 'Usage: /auth google add <email> [--replace] [--write] [--calendar]'
|
|
269
271
|
: 'Usage: /auth microsoft add <email> [--replace] [--org-mode]'
|
|
270
272
|
return {
|
|
271
273
|
kind: 'help',
|
|
@@ -280,7 +282,7 @@ export function parseProviderVerb(
|
|
|
280
282
|
if (!email) {
|
|
281
283
|
const usage =
|
|
282
284
|
provider === 'google'
|
|
283
|
-
? 'Usage: /auth google add <email> [--replace] [--write]'
|
|
285
|
+
? 'Usage: /auth google add <email> [--replace] [--write] [--calendar]'
|
|
284
286
|
: 'Usage: /auth microsoft add <email> [--replace] [--org-mode]'
|
|
285
287
|
return { kind: 'help', reason: usage }
|
|
286
288
|
}
|
|
@@ -288,7 +290,7 @@ export function parseProviderVerb(
|
|
|
288
290
|
if (emailErr) return { kind: 'help', reason: emailErr }
|
|
289
291
|
// Reject unknown flags so a typo (`--replaced`) isn't silently dropped.
|
|
290
292
|
const allowed = provider === 'google'
|
|
291
|
-
? new Set(['--replace', '--write'])
|
|
293
|
+
? new Set(['--replace', '--write', '--calendar'])
|
|
292
294
|
: new Set(['--replace', '--org-mode'])
|
|
293
295
|
for (const f of flags) {
|
|
294
296
|
if (!allowed.has(f)) {
|
|
@@ -304,6 +306,7 @@ export function parseProviderVerb(
|
|
|
304
306
|
email,
|
|
305
307
|
replace: flags.has('--replace'),
|
|
306
308
|
write: provider === 'google' && flags.has('--write'),
|
|
309
|
+
calendar: provider === 'google' && flags.has('--calendar'),
|
|
307
310
|
orgMode: provider === 'microsoft' && flags.has('--org-mode'),
|
|
308
311
|
}
|
|
309
312
|
}
|
|
@@ -84,6 +84,8 @@ export interface SpawnRelayOpts {
|
|
|
84
84
|
replace?: boolean
|
|
85
85
|
/** Google `--write` (Drive write scope). */
|
|
86
86
|
write?: boolean
|
|
87
|
+
/** Google `--calendar` (read-only Calendar scope). */
|
|
88
|
+
calendar?: boolean
|
|
87
89
|
/** Microsoft `--org-mode`. */
|
|
88
90
|
orgMode?: boolean
|
|
89
91
|
/** Override the CLI binary (tests / non-default install). */
|
|
@@ -96,6 +98,42 @@ export type SpawnRelay = (
|
|
|
96
98
|
opts: SpawnRelayOpts,
|
|
97
99
|
) => RelayChild
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Build the `switchroom auth <provider> account add` argv the relay spawns.
|
|
103
|
+
*
|
|
104
|
+
* Pure + exported so the flag plumbing is unit-pinned: every opt-in flag
|
|
105
|
+
* the chat command accepts has to actually reach the CLI, and a flag that
|
|
106
|
+
* was NOT requested must never appear (the relay is the only path most
|
|
107
|
+
* operators use, so a dropped `--calendar` here is indistinguishable from
|
|
108
|
+
* the feature not existing, and a spurious one silently widens a grant).
|
|
109
|
+
*/
|
|
110
|
+
export function buildRelayArgs(
|
|
111
|
+
provider: LoopbackProvider,
|
|
112
|
+
email: string,
|
|
113
|
+
opts: SpawnRelayOpts,
|
|
114
|
+
): string[] {
|
|
115
|
+
return provider === 'google'
|
|
116
|
+
? [
|
|
117
|
+
'auth',
|
|
118
|
+
'google',
|
|
119
|
+
'account',
|
|
120
|
+
'add',
|
|
121
|
+
email,
|
|
122
|
+
...(opts.replace ? ['--replace'] : []),
|
|
123
|
+
...(opts.write ? ['--write'] : []),
|
|
124
|
+
...(opts.calendar ? ['--calendar'] : []),
|
|
125
|
+
]
|
|
126
|
+
: [
|
|
127
|
+
'auth',
|
|
128
|
+
'microsoft',
|
|
129
|
+
'account',
|
|
130
|
+
'add',
|
|
131
|
+
email,
|
|
132
|
+
...(opts.replace ? ['--replace'] : []),
|
|
133
|
+
...(opts.orgMode ? ['--org-mode'] : []),
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
|
|
99
137
|
/**
|
|
100
138
|
* Default spawn: the real `switchroom` CLI, stdout+stderr piped, stdin
|
|
101
139
|
* ignored. `BROWSER=/bin/true` suppresses any browser auto-open; the Drive
|
|
@@ -109,26 +147,7 @@ export function defaultSpawnRelay(
|
|
|
109
147
|
opts: SpawnRelayOpts,
|
|
110
148
|
): RelayChild {
|
|
111
149
|
const binary = opts.binary ?? 'switchroom'
|
|
112
|
-
const args =
|
|
113
|
-
provider === 'google'
|
|
114
|
-
? [
|
|
115
|
-
'auth',
|
|
116
|
-
'google',
|
|
117
|
-
'account',
|
|
118
|
-
'add',
|
|
119
|
-
email,
|
|
120
|
-
...(opts.replace ? ['--replace'] : []),
|
|
121
|
-
...(opts.write ? ['--write'] : []),
|
|
122
|
-
]
|
|
123
|
-
: [
|
|
124
|
-
'auth',
|
|
125
|
-
'microsoft',
|
|
126
|
-
'account',
|
|
127
|
-
'add',
|
|
128
|
-
email,
|
|
129
|
-
...(opts.replace ? ['--replace'] : []),
|
|
130
|
-
...(opts.orgMode ? ['--org-mode'] : []),
|
|
131
|
-
]
|
|
150
|
+
const args = buildRelayArgs(provider, email, opts)
|
|
132
151
|
const child: ChildProcess = spawn(binary, args, {
|
|
133
152
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
134
153
|
env: {
|
|
@@ -20738,7 +20738,7 @@ bot.command("auth", async ctx => {
|
|
|
20738
20738
|
const { consentUrl, state, port, child } = await startLoopbackFlow(
|
|
20739
20739
|
parsed.provider,
|
|
20740
20740
|
parsed.email,
|
|
20741
|
-
{ replace: parsed.replace, write: parsed.write, orgMode: parsed.orgMode },
|
|
20741
|
+
{ replace: parsed.replace, write: parsed.write, calendar: parsed.calendar, orgMode: parsed.orgMode },
|
|
20742
20742
|
)
|
|
20743
20743
|
const newFlow = {
|
|
20744
20744
|
provider: parsed.provider,
|
|
@@ -22,6 +22,7 @@ import { describe, it, expect } from 'vitest'
|
|
|
22
22
|
import { EventEmitter } from 'node:events'
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
|
+
buildRelayArgs,
|
|
25
26
|
parseLoopbackRedirect,
|
|
26
27
|
extractConsentUrl,
|
|
27
28
|
parseConsentUrl,
|
|
@@ -492,6 +493,7 @@ describe('parseAuthCommand — provider verbs (#2582)', () => {
|
|
|
492
493
|
email: 'user@example.com',
|
|
493
494
|
replace: false,
|
|
494
495
|
write: false,
|
|
496
|
+
calendar: false,
|
|
495
497
|
orgMode: false,
|
|
496
498
|
})
|
|
497
499
|
})
|
|
@@ -501,6 +503,26 @@ describe('parseAuthCommand — provider verbs (#2582)', () => {
|
|
|
501
503
|
expect(p).toMatchObject({ kind: 'provider-add', replace: true, write: true })
|
|
502
504
|
})
|
|
503
505
|
|
|
506
|
+
it('parses google --calendar, and it does not imply --write', () => {
|
|
507
|
+
const p = parseAuthCommand('/auth google add user@example.com --replace --calendar')
|
|
508
|
+
expect(p).toMatchObject({
|
|
509
|
+
kind: 'provider-add',
|
|
510
|
+
replace: true,
|
|
511
|
+
calendar: true,
|
|
512
|
+
write: false,
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('parses --write and --calendar together', () => {
|
|
517
|
+
const p = parseAuthCommand('/auth google add user@example.com --write --calendar')
|
|
518
|
+
expect(p).toMatchObject({ kind: 'provider-add', write: true, calendar: true })
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it('--calendar is google-only — microsoft rejects it as an unknown flag', () => {
|
|
522
|
+
const p = parseAuthCommand('/auth microsoft add user@corp.com --calendar')
|
|
523
|
+
expect(p).toMatchObject({ kind: 'help' })
|
|
524
|
+
})
|
|
525
|
+
|
|
504
526
|
it('parses /auth microsoft add with --org-mode', () => {
|
|
505
527
|
const p = parseAuthCommand('/auth microsoft add user@corp.com --org-mode')
|
|
506
528
|
expect(p).toMatchObject({
|
|
@@ -531,3 +553,39 @@ describe('parseAuthCommand — provider verbs (#2582)', () => {
|
|
|
531
553
|
expect(p).toMatchObject({ kind: 'help' })
|
|
532
554
|
})
|
|
533
555
|
})
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
describe('buildRelayArgs — chat flags actually reach the CLI', () => {
|
|
559
|
+
it('bare google add: no opt-in flags', () => {
|
|
560
|
+
expect(buildRelayArgs('google', 'a@b.com', {})).toEqual([
|
|
561
|
+
'auth', 'google', 'account', 'add', 'a@b.com',
|
|
562
|
+
])
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('forwards --calendar', () => {
|
|
566
|
+
expect(buildRelayArgs('google', 'a@b.com', { calendar: true })).toEqual([
|
|
567
|
+
'auth', 'google', 'account', 'add', 'a@b.com', '--calendar',
|
|
568
|
+
])
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('forwards --replace --write --calendar together, in order', () => {
|
|
572
|
+
expect(
|
|
573
|
+
buildRelayArgs('google', 'a@b.com', {
|
|
574
|
+
replace: true,
|
|
575
|
+
write: true,
|
|
576
|
+
calendar: true,
|
|
577
|
+
}),
|
|
578
|
+
).toEqual([
|
|
579
|
+
'auth', 'google', 'account', 'add', 'a@b.com',
|
|
580
|
+
'--replace', '--write', '--calendar',
|
|
581
|
+
])
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('--calendar never leaks into the microsoft argv', () => {
|
|
585
|
+
expect(
|
|
586
|
+
buildRelayArgs('microsoft', 'a@corp.com', { calendar: true, orgMode: true }),
|
|
587
|
+
).toEqual([
|
|
588
|
+
'auth', 'microsoft', 'account', 'add', 'a@corp.com', '--org-mode',
|
|
589
|
+
])
|
|
590
|
+
})
|
|
591
|
+
})
|