switchroom 0.18.29 → 0.18.31

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 (40) hide show
  1. package/bin/handoff-briefing.sh +8 -2
  2. package/dist/agent-scheduler/index.js +111 -7
  3. package/dist/auth-broker/index.js +154 -16
  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 +2089 -1587
  9. package/dist/host-control/main.js +110 -13
  10. package/dist/vault/approvals/kernel-server.js +116 -13
  11. package/dist/vault/broker/server.js +314 -145
  12. package/package.json +3 -3
  13. package/profiles/_base/start.sh.hbs +172 -22
  14. package/telegram-plugin/dist/bridge/bridge.js +71 -47
  15. package/telegram-plugin/dist/gateway/gateway.js +601 -104
  16. package/telegram-plugin/dist/server.js +89 -64
  17. package/telegram-plugin/gateway/gateway.ts +280 -33
  18. package/telegram-plugin/gateway/model-command.ts +104 -0
  19. package/telegram-plugin/gateway/session-model-file.ts +40 -0
  20. package/telegram-plugin/gateway/turn-flush-suppression.ts +82 -0
  21. package/telegram-plugin/gateway/unhandled-message.ts +177 -0
  22. package/telegram-plugin/llm-error-present.ts +24 -0
  23. package/telegram-plugin/model-unavailable.ts +55 -0
  24. package/telegram-plugin/operator-events.ts +113 -0
  25. package/telegram-plugin/pending-user-notice.ts +88 -0
  26. package/telegram-plugin/shared/local-time.ts +43 -0
  27. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
  28. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
  29. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +26 -2
  30. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
  31. package/telegram-plugin/tests/local-time.test.ts +68 -1
  32. package/telegram-plugin/tests/model-command.test.ts +133 -0
  33. package/telegram-plugin/tests/session-model-file.test.ts +23 -0
  34. package/telegram-plugin/tests/turn-flush-suppression.test.ts +90 -0
  35. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
  36. package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
  37. package/vendor/hindsight-memory/scripts/lib/content.py +53 -1
  38. package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
  39. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
  40. package/vendor/hindsight-memory/tests/test_content.py +35 -0
@@ -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";
@@ -21102,6 +21193,12 @@ var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
21102
21193
  var DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin";
21103
21194
  var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
21104
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
+
21105
21202
  // src/agents/compose.ts
21106
21203
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
21107
21204
 
@@ -26548,7 +26645,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
26548
26645
  import { dirname as dirname4, join as join7 } from "node:path";
26549
26646
 
26550
26647
  // src/build-info.ts
26551
- var VERSION = "0.18.29";
26648
+ var VERSION = "0.18.31";
26552
26649
 
26553
26650
  // src/cli/resolve-version.ts
26554
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";
@@ -19069,10 +19165,17 @@ var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
19069
19165
 
19070
19166
  // src/agents/compose.ts
19071
19167
  init_merge();
19168
+ init_timezone();
19072
19169
 
19073
19170
  // src/setup/host-capabilities.ts
19074
19171
  init_paths();
19075
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
+
19076
19179
  // src/agents/compose.ts
19077
19180
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
19078
19181