switchroom 0.18.28 → 0.18.30

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.
Files changed (48) hide show
  1. package/bin/handoff-briefing.sh +15 -2
  2. package/dist/agent-scheduler/index.js +111 -7
  3. package/dist/auth-broker/index.js +154 -73
  4. package/dist/cli/autoaccept-poll.js +8 -3
  5. package/dist/cli/drive-write-pretool.mjs +8 -3
  6. package/dist/cli/ms-365-write-pretool.mjs +158 -11
  7. package/dist/cli/notion-write-pretool.mjs +103 -4
  8. package/dist/cli/switchroom.js +2712 -2219
  9. package/dist/host-control/main.js +110 -70
  10. package/dist/vault/approvals/kernel-server.js +116 -70
  11. package/dist/vault/broker/server.js +314 -202
  12. package/package.json +3 -3
  13. package/profiles/_base/start.sh.hbs +105 -34
  14. package/telegram-plugin/dist/bridge/bridge.js +71 -47
  15. package/telegram-plugin/dist/gateway/gateway.js +1128 -666
  16. package/telegram-plugin/dist/server.js +89 -64
  17. package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
  18. package/telegram-plugin/gateway/forward-origin.ts +9 -1
  19. package/telegram-plugin/gateway/gateway.ts +656 -388
  20. package/telegram-plugin/gateway/model-command.ts +331 -602
  21. package/telegram-plugin/gateway/session-model-file.ts +40 -0
  22. package/telegram-plugin/gateway/turn-record-status.ts +45 -0
  23. package/telegram-plugin/gateway/unhandled-message.ts +177 -0
  24. package/telegram-plugin/history.ts +153 -23
  25. package/telegram-plugin/llm-error-present.ts +24 -0
  26. package/telegram-plugin/model-unavailable.ts +55 -0
  27. package/telegram-plugin/operator-events.ts +113 -0
  28. package/telegram-plugin/pending-user-notice.ts +88 -0
  29. package/telegram-plugin/shared/local-time.ts +99 -0
  30. package/telegram-plugin/tests/backstop-delivery.test.ts +250 -0
  31. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
  32. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
  33. package/telegram-plugin/tests/forward-origin.test.ts +30 -3
  34. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +111 -60
  35. package/telegram-plugin/tests/history.test.ts +88 -0
  36. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
  37. package/telegram-plugin/tests/local-time.test.ts +135 -0
  38. package/telegram-plugin/tests/model-command.test.ts +427 -1512
  39. package/telegram-plugin/tests/session-model-file.test.ts +23 -0
  40. package/telegram-plugin/tests/turn-flush-safety.test.ts +34 -0
  41. package/telegram-plugin/tier-downgrade.ts +4 -3
  42. package/telegram-plugin/turn-flush-safety.ts +25 -1
  43. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
  44. package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
  45. package/vendor/hindsight-memory/scripts/lib/content.py +93 -7
  46. package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
  47. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
  48. package/vendor/hindsight-memory/tests/test_content.py +63 -7
@@ -18991,11 +18991,52 @@ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
18991
18991
  approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
18992
18992
  tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
18993
18993
  }).optional();
18994
+ var MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
18995
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
18996
+ }).transform((v) => v.trim().toLowerCase());
18997
+ var MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
18998
+ message: "microsoft_workspace tools[] tokens are forwarded to softeria's " + "--enabled-tools regex; each must be lowercase kebab-case " + "([a-z0-9-]+, e.g. 'mail', 'calendar', 'send-mail') so an unescaped " + "regex metacharacter can't crash the launcher"
18999
+ });
19000
+ var MicrosoftAccountBindingSchema = exports_external.object({
19001
+ account: MicrosoftAccountEmailSchema.describe("The Microsoft account this binding uses. Must be a key in top-level " + "`microsoft_accounts:` with this agent in its `enabled_for[]`."),
19002
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist → softeria `--enabled-tools <regex>` " + "(tokens joined with `|`). Omitted = all tools exposed for this account."),
19003
+ org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 §6.4).")
19004
+ });
18994
19005
  var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
18995
- account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
18996
- message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
18997
- }).transform((v) => v.trim().toLowerCase()).optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized)."),
18998
- org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
19006
+ account: MicrosoftAccountEmailSchema.optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized). Mutually exclusive with `accounts` (plural)."),
19007
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist for the SINGULAR `account` form " + "softeria `--enabled-tools <regex>`. Omitted = all tools. Only " + "valid with the singular `account`; using it together with the " + "plural `accounts` (which carries per-binding `tools`) is an error."),
19008
+ org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false)."),
19009
+ accounts: exports_external.array(MicrosoftAccountBindingSchema).min(1).optional().describe("Plural multi-account form: bind MULTIPLE Microsoft accounts to " + "this agent, each with its own tool scope. Mutually exclusive with " + "the singular `account`. Each account gets its own `ms-365-<slug>` " + "MCP server.")
19010
+ }).superRefine((v, ctx) => {
19011
+ const hasSingular = v.account !== undefined;
19012
+ const hasPlural = v.accounts !== undefined;
19013
+ if (hasSingular && hasPlural) {
19014
+ ctx.addIssue({
19015
+ code: exports_external.ZodIssueCode.custom,
19016
+ message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
19017
+ path: ["accounts"]
19018
+ });
19019
+ }
19020
+ if (hasPlural && v.tools !== undefined) {
19021
+ ctx.addIssue({
19022
+ code: exports_external.ZodIssueCode.custom,
19023
+ message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
19024
+ path: ["tools"]
19025
+ });
19026
+ }
19027
+ if (hasPlural) {
19028
+ const seen = new Set;
19029
+ for (const b of v.accounts) {
19030
+ if (seen.has(b.account)) {
19031
+ ctx.addIssue({
19032
+ code: exports_external.ZodIssueCode.custom,
19033
+ message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
19034
+ path: ["accounts"]
19035
+ });
19036
+ }
19037
+ seen.add(b.account);
19038
+ }
19039
+ }
18999
19040
  }).optional();
19000
19041
  var AgentNotionWorkspaceConfigSchema = exports_external.object({
19001
19042
  databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
@@ -19370,6 +19411,26 @@ var SwitchroomConfigSchema = exports_external.object({
19370
19411
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
19371
19412
  checkServes(a.serves, ["agents", name, "serves"]);
19372
19413
  }
19414
+ const microsoftAccounts = cfg.microsoft_accounts;
19415
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
19416
+ const mw = a.microsoft_workspace;
19417
+ const accounts = mw?.accounts;
19418
+ if (!accounts)
19419
+ continue;
19420
+ accounts.forEach((b, i) => {
19421
+ const acct = b?.account?.trim().toLowerCase();
19422
+ if (!acct)
19423
+ return;
19424
+ const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
19425
+ if (!enabledFor.includes(name)) {
19426
+ ctx.addIssue({
19427
+ code: exports_external.ZodIssueCode.custom,
19428
+ message: `agent '${name}' binds Microsoft account '${acct}' but is not in ` + `microsoft_accounts['${acct}'].enabled_for[] — operator must run ` + `\`switchroom auth microsoft enable ${acct} ${name}\``,
19429
+ path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
19430
+ });
19431
+ }
19432
+ });
19433
+ }
19373
19434
  });
19374
19435
 
19375
19436
  // src/config/paths.ts
@@ -19974,6 +20035,22 @@ function validateNotionWorkspaceConfig(config) {
19974
20035
  return issues;
19975
20036
  }
19976
20037
 
20038
+ // src/config/timezone.ts
20039
+ function isResolvableTimezone(zone) {
20040
+ try {
20041
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
20042
+ return true;
20043
+ } catch {
20044
+ return false;
20045
+ }
20046
+ }
20047
+ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
20048
+ "UTC",
20049
+ "Etc/UTC",
20050
+ "Etc/Universal",
20051
+ "Universal"
20052
+ ]);
20053
+
19977
20054
  // src/config/loader.ts
19978
20055
  class ConfigError extends Error {
19979
20056
  details;
@@ -20097,8 +20174,30 @@ function loadConfig(configPath) {
20097
20174
  if (notionIssues.length > 0) {
20098
20175
  throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
20099
20176
  }
20177
+ validateAllTimezones(config, filePath);
20100
20178
  return config;
20101
20179
  }
20180
+ function validateAllTimezones(config, filePath) {
20181
+ const issues = [];
20182
+ const check = (zone, where) => {
20183
+ if (zone == null)
20184
+ return;
20185
+ if (!isResolvableTimezone(zone)) {
20186
+ issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists — check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
20187
+ }
20188
+ };
20189
+ check(config.switchroom?.timezone, "switchroom.timezone");
20190
+ check(config.defaults?.timezone, "defaults.timezone");
20191
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
20192
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
20193
+ }
20194
+ for (const [agentName, agentRaw] of Object.entries(config.agents)) {
20195
+ check(agentRaw?.timezone, `agents.${agentName}.timezone`);
20196
+ }
20197
+ if (issues.length > 0) {
20198
+ throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
20199
+ }
20200
+ }
20102
20201
  function validateAllCronTopicAliases(config, filePath) {
20103
20202
  const issues = [];
20104
20203
  for (const [agentName, agentRaw] of Object.entries(config.agents)) {
@@ -20568,14 +20667,6 @@ function allocateAgentUid(name) {
20568
20667
  return AGENT_UID_MIN + u32 % range;
20569
20668
  }
20570
20669
 
20571
- // src/config/timezone.ts
20572
- var CONTAINER_DEFAULT_UTC_ZONES = new Set([
20573
- "UTC",
20574
- "Etc/UTC",
20575
- "Etc/Universal",
20576
- "Universal"
20577
- ]);
20578
-
20579
20670
  // src/cli/agent-config.ts
20580
20671
  import { join as join2 } from "node:path";
20581
20672
  import { homedir as homedir2 } from "node:os";
@@ -20610,63 +20701,6 @@ for (const name of SHARED_FRAGMENTS) {
20610
20701
  }
20611
20702
  }
20612
20703
 
20613
- // src/agents/pane-lock.ts
20614
- var tails = new Map;
20615
-
20616
- // src/agents/inject.ts
20617
- var INJECT_COMMANDS = new Map([
20618
- ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
20619
- ["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
20620
- ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
20621
- ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true, argsAllowed: false }],
20622
- ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true, argsAllowed: false }],
20623
- ["/help", { description: "Show help / command discovery", expectsOutput: true, dialog: true, argsAllowed: false }],
20624
- ["/context", { description: "Show context window usage", expectsOutput: true, argsAllowed: false }],
20625
- ["/release-notes", { description: "Show release-notes version list", expectsOutput: true, dialog: true, argsAllowed: false }],
20626
- ["/model", { description: "Open model picker", expectsOutput: true, argsAllowed: true }],
20627
- [
20628
- "/clear",
20629
- {
20630
- description: "Clear session screen",
20631
- expectsOutput: false,
20632
- silentNote: "context cleared — fresh slate",
20633
- argsAllowed: false
20634
- }
20635
- ],
20636
- [
20637
- "/compact",
20638
- {
20639
- description: "Compact conversation history",
20640
- expectsOutput: false,
20641
- silentNote: "compaction runs silently",
20642
- argsAllowed: false
20643
- }
20644
- ]
20645
- ]);
20646
- var INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
20647
- var INJECT_BLOCKED = new Map([
20648
- ["/login", { reason: "would mutate auth state" }],
20649
- ["/logout", { reason: "would terminate the agent's auth session" }],
20650
- ["/exit", { reason: "would kill the agent process" }],
20651
- ["/quit", { reason: "would kill the agent process" }],
20652
- ["/upgrade", { reason: "mutates the Claude Code installation" }],
20653
- ["/init", { reason: "generates/overwrites CLAUDE.md and runs a model turn" }],
20654
- ["/mcp", { reason: "opens an interactive MCP server management dialog" }],
20655
- ["/permissions", { reason: "opens an interactive permissions editor that mutates tool policy" }],
20656
- ["/install-github-app", { reason: "runs a network/OAuth install flow" }],
20657
- ["/add-dir", { reason: "mutates the session's working-directory set" }],
20658
- ["/terminal-setup", { reason: "mutates terminal keybinding configuration" }],
20659
- ["/privacy-settings", { reason: "opens an interactive privacy-settings dialog" }],
20660
- ["/bug", { reason: "submits a bug report over the network" }],
20661
- [
20662
- "/effort",
20663
- {
20664
- reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
20665
- }
20666
- ]
20667
- ]);
20668
- var INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
20669
-
20670
20704
  // src/setup/hindsight.ts
20671
20705
  var HINDSIGHT_DEFAULT_API_PORT = 18888;
20672
20706
  var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
@@ -21159,6 +21193,12 @@ var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
21159
21193
  var DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin";
21160
21194
  var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
21161
21195
 
21196
+ // src/vault/grants-db-path.ts
21197
+ var GRANTS_DB_DIRNAME = "vault-broker";
21198
+ var GRANTS_DB_FILENAME = "vault-grants.db";
21199
+ var GRANTS_DB_CONTAINER_DIR = `/root/.switchroom/${GRANTS_DB_DIRNAME}`;
21200
+ var GRANTS_DB_CONTAINER_PATH = `${GRANTS_DB_CONTAINER_DIR}/${GRANTS_DB_FILENAME}`;
21201
+
21162
21202
  // src/agents/compose.ts
21163
21203
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
21164
21204
 
@@ -26605,7 +26645,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
26605
26645
  import { dirname as dirname4, join as join7 } from "node:path";
26606
26646
 
26607
26647
  // src/build-info.ts
26608
- var VERSION = "0.18.28";
26648
+ var VERSION = "0.18.30";
26609
26649
 
26610
26650
  // src/cli/resolve-version.ts
26611
26651
  function readPackageVersion() {
@@ -4013,7 +4013,7 @@ var init_zod = __esm(() => {
4013
4013
  });
4014
4014
 
4015
4015
  // src/config/schema.ts
4016
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
4016
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
4017
4017
  var init_schema = __esm(() => {
4018
4018
  init_zod();
4019
4019
  CodeRepoEntrySchema = exports_external.object({
@@ -4411,11 +4411,52 @@ var init_schema = __esm(() => {
4411
4411
  approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
4412
4412
  tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
4413
4413
  }).optional();
4414
+ MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
4415
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
4416
+ }).transform((v) => v.trim().toLowerCase());
4417
+ MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
4418
+ message: "microsoft_workspace tools[] tokens are forwarded to softeria's " + "--enabled-tools regex; each must be lowercase kebab-case " + "([a-z0-9-]+, e.g. 'mail', 'calendar', 'send-mail') so an unescaped " + "regex metacharacter can't crash the launcher"
4419
+ });
4420
+ MicrosoftAccountBindingSchema = exports_external.object({
4421
+ account: MicrosoftAccountEmailSchema.describe("The Microsoft account this binding uses. Must be a key in top-level " + "`microsoft_accounts:` with this agent in its `enabled_for[]`."),
4422
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist → softeria `--enabled-tools <regex>` " + "(tokens joined with `|`). Omitted = all tools exposed for this account."),
4423
+ org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 §6.4).")
4424
+ });
4414
4425
  AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
4415
- account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
4416
- message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
4417
- }).transform((v) => v.trim().toLowerCase()).optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized)."),
4418
- org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
4426
+ account: MicrosoftAccountEmailSchema.optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized). Mutually exclusive with `accounts` (plural)."),
4427
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist for the SINGULAR `account` form " + "softeria `--enabled-tools <regex>`. Omitted = all tools. Only " + "valid with the singular `account`; using it together with the " + "plural `accounts` (which carries per-binding `tools`) is an error."),
4428
+ org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false)."),
4429
+ accounts: exports_external.array(MicrosoftAccountBindingSchema).min(1).optional().describe("Plural multi-account form: bind MULTIPLE Microsoft accounts to " + "this agent, each with its own tool scope. Mutually exclusive with " + "the singular `account`. Each account gets its own `ms-365-<slug>` " + "MCP server.")
4430
+ }).superRefine((v, ctx) => {
4431
+ const hasSingular = v.account !== undefined;
4432
+ const hasPlural = v.accounts !== undefined;
4433
+ if (hasSingular && hasPlural) {
4434
+ ctx.addIssue({
4435
+ code: exports_external.ZodIssueCode.custom,
4436
+ message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
4437
+ path: ["accounts"]
4438
+ });
4439
+ }
4440
+ if (hasPlural && v.tools !== undefined) {
4441
+ ctx.addIssue({
4442
+ code: exports_external.ZodIssueCode.custom,
4443
+ message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
4444
+ path: ["tools"]
4445
+ });
4446
+ }
4447
+ if (hasPlural) {
4448
+ const seen = new Set;
4449
+ for (const b of v.accounts) {
4450
+ if (seen.has(b.account)) {
4451
+ ctx.addIssue({
4452
+ code: exports_external.ZodIssueCode.custom,
4453
+ message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
4454
+ path: ["accounts"]
4455
+ });
4456
+ }
4457
+ seen.add(b.account);
4458
+ }
4459
+ }
4419
4460
  }).optional();
4420
4461
  AgentNotionWorkspaceConfigSchema = exports_external.object({
4421
4462
  databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
@@ -4790,6 +4831,26 @@ var init_schema = __esm(() => {
4790
4831
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
4791
4832
  checkServes(a.serves, ["agents", name, "serves"]);
4792
4833
  }
4834
+ const microsoftAccounts = cfg.microsoft_accounts;
4835
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
4836
+ const mw = a.microsoft_workspace;
4837
+ const accounts = mw?.accounts;
4838
+ if (!accounts)
4839
+ continue;
4840
+ accounts.forEach((b, i) => {
4841
+ const acct = b?.account?.trim().toLowerCase();
4842
+ if (!acct)
4843
+ return;
4844
+ const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
4845
+ if (!enabledFor.includes(name)) {
4846
+ ctx.addIssue({
4847
+ code: exports_external.ZodIssueCode.custom,
4848
+ message: `agent '${name}' binds Microsoft account '${acct}' but is not in ` + `microsoft_accounts['${acct}'].enabled_for[] — operator must run ` + `\`switchroom auth microsoft enable ${acct} ${name}\``,
4849
+ path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
4850
+ });
4851
+ }
4852
+ });
4853
+ }
4793
4854
  });
4794
4855
  });
4795
4856
 
@@ -5184,6 +5245,25 @@ var init_merge = __esm(() => {
5184
5245
  })(mergeAgentConfig ||= {});
5185
5246
  });
5186
5247
 
5248
+ // src/config/timezone.ts
5249
+ function isResolvableTimezone(zone) {
5250
+ try {
5251
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
5252
+ return true;
5253
+ } catch {
5254
+ return false;
5255
+ }
5256
+ }
5257
+ var CONTAINER_DEFAULT_UTC_ZONES;
5258
+ var init_timezone = __esm(() => {
5259
+ CONTAINER_DEFAULT_UTC_ZONES = new Set([
5260
+ "UTC",
5261
+ "Etc/UTC",
5262
+ "Etc/Universal",
5263
+ "Universal"
5264
+ ]);
5265
+ });
5266
+
5187
5267
  // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js
5188
5268
  var require_identity = __commonJS((exports) => {
5189
5269
  var ALIAS = Symbol.for("yaml.alias");
@@ -12492,8 +12572,30 @@ function loadConfig(configPath) {
12492
12572
  if (notionIssues.length > 0) {
12493
12573
  throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
12494
12574
  }
12575
+ validateAllTimezones(config, filePath);
12495
12576
  return config;
12496
12577
  }
12578
+ function validateAllTimezones(config, filePath) {
12579
+ const issues = [];
12580
+ const check = (zone, where) => {
12581
+ if (zone == null)
12582
+ return;
12583
+ if (!isResolvableTimezone(zone)) {
12584
+ issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists — check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
12585
+ }
12586
+ };
12587
+ check(config.switchroom?.timezone, "switchroom.timezone");
12588
+ check(config.defaults?.timezone, "defaults.timezone");
12589
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
12590
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
12591
+ }
12592
+ for (const [agentName, agentRaw] of Object.entries(config.agents)) {
12593
+ check(agentRaw?.timezone, `agents.${agentName}.timezone`);
12594
+ }
12595
+ if (issues.length > 0) {
12596
+ throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
12597
+ }
12598
+ }
12497
12599
  function validateAllCronTopicAliases(config, filePath) {
12498
12600
  const issues = [];
12499
12601
  for (const [agentName, agentRaw] of Object.entries(config.agents)) {
@@ -12536,6 +12638,7 @@ var init_loader = __esm(() => {
12536
12638
  init_paths();
12537
12639
  init_overlay_loader();
12538
12640
  init_merge();
12641
+ init_timezone();
12539
12642
  import_yaml2 = __toESM(require_dist(), 1);
12540
12643
  ConfigError = class ConfigError extends Error {
12541
12644
  details;
@@ -18937,14 +19040,7 @@ init_merge();
18937
19040
 
18938
19041
  // src/agents/scaffold.ts
18939
19042
  init_merge();
18940
-
18941
- // src/config/timezone.ts
18942
- var CONTAINER_DEFAULT_UTC_ZONES = new Set([
18943
- "UTC",
18944
- "Etc/UTC",
18945
- "Etc/Universal",
18946
- "Universal"
18947
- ]);
19043
+ init_timezone();
18948
19044
 
18949
19045
  // src/cli/agent-config.ts
18950
19046
  import { join } from "node:path";
@@ -18990,63 +19086,6 @@ for (const name of SHARED_FRAGMENTS) {
18990
19086
  }
18991
19087
  }
18992
19088
 
18993
- // src/agents/pane-lock.ts
18994
- var tails = new Map;
18995
-
18996
- // src/agents/inject.ts
18997
- var INJECT_COMMANDS = new Map([
18998
- ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
18999
- ["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
19000
- ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
19001
- ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true, argsAllowed: false }],
19002
- ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true, argsAllowed: false }],
19003
- ["/help", { description: "Show help / command discovery", expectsOutput: true, dialog: true, argsAllowed: false }],
19004
- ["/context", { description: "Show context window usage", expectsOutput: true, argsAllowed: false }],
19005
- ["/release-notes", { description: "Show release-notes version list", expectsOutput: true, dialog: true, argsAllowed: false }],
19006
- ["/model", { description: "Open model picker", expectsOutput: true, argsAllowed: true }],
19007
- [
19008
- "/clear",
19009
- {
19010
- description: "Clear session screen",
19011
- expectsOutput: false,
19012
- silentNote: "context cleared — fresh slate",
19013
- argsAllowed: false
19014
- }
19015
- ],
19016
- [
19017
- "/compact",
19018
- {
19019
- description: "Compact conversation history",
19020
- expectsOutput: false,
19021
- silentNote: "compaction runs silently",
19022
- argsAllowed: false
19023
- }
19024
- ]
19025
- ]);
19026
- var INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
19027
- var INJECT_BLOCKED = new Map([
19028
- ["/login", { reason: "would mutate auth state" }],
19029
- ["/logout", { reason: "would terminate the agent's auth session" }],
19030
- ["/exit", { reason: "would kill the agent process" }],
19031
- ["/quit", { reason: "would kill the agent process" }],
19032
- ["/upgrade", { reason: "mutates the Claude Code installation" }],
19033
- ["/init", { reason: "generates/overwrites CLAUDE.md and runs a model turn" }],
19034
- ["/mcp", { reason: "opens an interactive MCP server management dialog" }],
19035
- ["/permissions", { reason: "opens an interactive permissions editor that mutates tool policy" }],
19036
- ["/install-github-app", { reason: "runs a network/OAuth install flow" }],
19037
- ["/add-dir", { reason: "mutates the session's working-directory set" }],
19038
- ["/terminal-setup", { reason: "mutates terminal keybinding configuration" }],
19039
- ["/privacy-settings", { reason: "opens an interactive privacy-settings dialog" }],
19040
- ["/bug", { reason: "submits a bug report over the network" }],
19041
- [
19042
- "/effort",
19043
- {
19044
- reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
19045
- }
19046
- ]
19047
- ]);
19048
- var INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
19049
-
19050
19089
  // src/setup/hindsight.ts
19051
19090
  var HINDSIGHT_DEFAULT_API_PORT = 18888;
19052
19091
  var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
@@ -19126,10 +19165,17 @@ var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
19126
19165
 
19127
19166
  // src/agents/compose.ts
19128
19167
  init_merge();
19168
+ init_timezone();
19129
19169
 
19130
19170
  // src/setup/host-capabilities.ts
19131
19171
  init_paths();
19132
19172
 
19173
+ // src/vault/grants-db-path.ts
19174
+ var GRANTS_DB_DIRNAME = "vault-broker";
19175
+ var GRANTS_DB_FILENAME = "vault-grants.db";
19176
+ var GRANTS_DB_CONTAINER_DIR = `/root/.switchroom/${GRANTS_DB_DIRNAME}`;
19177
+ var GRANTS_DB_CONTAINER_PATH = `${GRANTS_DB_CONTAINER_DIR}/${GRANTS_DB_FILENAME}`;
19178
+
19133
19179
  // src/agents/compose.ts
19134
19180
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
19135
19181