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
@@ -135,8 +135,14 @@ $RECALL_TEXT"
135
135
  fi
136
136
 
137
137
  # ── Source 3: Today's daily memory ─────────────────────────────────────────────
138
+ # Resolve "today" in the agent's LOCAL time — NOT the process default (UTC on
139
+ # most hosts/CI). TODAY keys the daily-memory lookup (memory/${TODAY}.md); using
140
+ # UTC here would look up the wrong day's file during the window where the local
141
+ # date is ahead of/behind UTC, silently dropping today's memory. Same
142
+ # SWITCHROOM_TIMEZONE → TZ → UTC cascade the restart-timestamp render below uses.
143
+ _TZ_VAL="${SWITCHROOM_TIMEZONE:-${TZ:-UTC}}"
138
144
  DAILY_SECTION=""
139
- TODAY=$(date +%Y-%m-%d 2>/dev/null || true)
145
+ TODAY=$(TZ="$_TZ_VAL" date +%Y-%m-%d 2>/dev/null || date +%Y-%m-%d 2>/dev/null || true)
140
146
  if [ -n "$TODAY" ] && [ -n "$WORKSPACE_DIR" ]; then
141
147
  DAILY_FILE="$WORKSPACE_DIR/memory/${TODAY}.md"
142
148
  if [ -f "$DAILY_FILE" ] && [ -s "$DAILY_FILE" ]; then
@@ -156,7 +162,7 @@ fi
156
162
  # UTC "now" (the whole point of the deterministic-local-time work). Same
157
163
  # SWITCHROOM_TIMEZONE → TZ → UTC cascade and `%A %Y-%m-%d %I:%M %p %Z` am/pm
158
164
  # format the UserPromptSubmit local-time hook (bin/timezone-hook.sh) uses.
159
- _TZ_VAL="${SWITCHROOM_TIMEZONE:-${TZ:-UTC}}"
165
+ # (_TZ_VAL is computed once above, in the daily-memory section.)
160
166
  TIMESTAMP=$(TZ="$_TZ_VAL" date '+%A %Y-%m-%d %I:%M %p %Z' 2>/dev/null || date '+%A %Y-%m-%d %I:%M %p %Z')
161
167
 
162
168
  # Determine restart reason if available
@@ -11354,11 +11354,52 @@ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
11354
11354
  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."),
11355
11355
  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.")
11356
11356
  }).optional();
11357
+ var MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11358
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
11359
+ }).transform((v) => v.trim().toLowerCase());
11360
+ var MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
11361
+ 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"
11362
+ });
11363
+ var MicrosoftAccountBindingSchema = exports_external.object({
11364
+ 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[]`."),
11365
+ 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."),
11366
+ org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 §6.4).")
11367
+ });
11357
11368
  var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
11358
- account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11359
- message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
11360
- }).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)."),
11361
- 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).")
11369
+ 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)."),
11370
+ 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."),
11371
+ 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)."),
11372
+ 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.")
11373
+ }).superRefine((v, ctx) => {
11374
+ const hasSingular = v.account !== undefined;
11375
+ const hasPlural = v.accounts !== undefined;
11376
+ if (hasSingular && hasPlural) {
11377
+ ctx.addIssue({
11378
+ code: exports_external.ZodIssueCode.custom,
11379
+ message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
11380
+ path: ["accounts"]
11381
+ });
11382
+ }
11383
+ if (hasPlural && v.tools !== undefined) {
11384
+ ctx.addIssue({
11385
+ code: exports_external.ZodIssueCode.custom,
11386
+ message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
11387
+ path: ["tools"]
11388
+ });
11389
+ }
11390
+ if (hasPlural) {
11391
+ const seen = new Set;
11392
+ for (const b of v.accounts) {
11393
+ if (seen.has(b.account)) {
11394
+ ctx.addIssue({
11395
+ code: exports_external.ZodIssueCode.custom,
11396
+ message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
11397
+ path: ["accounts"]
11398
+ });
11399
+ }
11400
+ seen.add(b.account);
11401
+ }
11402
+ }
11362
11403
  }).optional();
11363
11404
  var AgentNotionWorkspaceConfigSchema = exports_external.object({
11364
11405
  databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
@@ -11733,6 +11774,26 @@ var SwitchroomConfigSchema = exports_external.object({
11733
11774
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11734
11775
  checkServes(a.serves, ["agents", name, "serves"]);
11735
11776
  }
11777
+ const microsoftAccounts = cfg.microsoft_accounts;
11778
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11779
+ const mw = a.microsoft_workspace;
11780
+ const accounts = mw?.accounts;
11781
+ if (!accounts)
11782
+ continue;
11783
+ accounts.forEach((b, i) => {
11784
+ const acct = b?.account?.trim().toLowerCase();
11785
+ if (!acct)
11786
+ return;
11787
+ const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
11788
+ if (!enabledFor.includes(name)) {
11789
+ ctx.addIssue({
11790
+ code: exports_external.ZodIssueCode.custom,
11791
+ 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}\``,
11792
+ path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
11793
+ });
11794
+ }
11795
+ });
11796
+ }
11736
11797
  });
11737
11798
 
11738
11799
  // src/config/paths.ts
@@ -12326,6 +12387,22 @@ function validateNotionWorkspaceConfig(config) {
12326
12387
  return issues;
12327
12388
  }
12328
12389
 
12390
+ // src/config/timezone.ts
12391
+ function isResolvableTimezone(zone) {
12392
+ try {
12393
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
12394
+ return true;
12395
+ } catch {
12396
+ return false;
12397
+ }
12398
+ }
12399
+ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
12400
+ "UTC",
12401
+ "Etc/UTC",
12402
+ "Etc/Universal",
12403
+ "Universal"
12404
+ ]);
12405
+
12329
12406
  // src/config/loader.ts
12330
12407
  class ConfigError extends Error {
12331
12408
  details;
@@ -12449,8 +12526,30 @@ function loadConfig(configPath) {
12449
12526
  if (notionIssues.length > 0) {
12450
12527
  throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
12451
12528
  }
12529
+ validateAllTimezones(config, filePath);
12452
12530
  return config;
12453
12531
  }
12532
+ function validateAllTimezones(config, filePath) {
12533
+ const issues = [];
12534
+ const check = (zone, where) => {
12535
+ if (zone == null)
12536
+ return;
12537
+ if (!isResolvableTimezone(zone)) {
12538
+ 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").`);
12539
+ }
12540
+ };
12541
+ check(config.switchroom?.timezone, "switchroom.timezone");
12542
+ check(config.defaults?.timezone, "defaults.timezone");
12543
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
12544
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
12545
+ }
12546
+ for (const [agentName, agentRaw] of Object.entries(config.agents)) {
12547
+ check(agentRaw?.timezone, `agents.${agentName}.timezone`);
12548
+ }
12549
+ if (issues.length > 0) {
12550
+ throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
12551
+ }
12552
+ }
12454
12553
  function validateAllCronTopicAliases(config, filePath) {
12455
12554
  const issues = [];
12456
12555
  for (const [agentName, agentRaw] of Object.entries(config.agents)) {
@@ -13691,7 +13790,8 @@ var GetCredentialsRequestSchema = exports_external.object({
13691
13790
  v: exports_external.literal(PROTOCOL_VERSION),
13692
13791
  op: exports_external.literal("get-credentials"),
13693
13792
  id: exports_external.string().min(1),
13694
- provider: ProviderNameSchema.optional()
13793
+ provider: ProviderNameSchema.optional(),
13794
+ account: exports_external.string().min(1).optional()
13695
13795
  });
13696
13796
  var ListStateRequestSchema = exports_external.object({
13697
13797
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -14033,13 +14133,17 @@ class AuthBrokerClient {
14033
14133
  sock.destroy();
14034
14134
  }
14035
14135
  }
14036
- async getCredentials(provider) {
14136
+ async getCredentials(provider, account) {
14037
14137
  const base = {
14038
14138
  v: PROTOCOL_VERSION,
14039
14139
  id: randomUUID(),
14040
14140
  op: "get-credentials"
14041
14141
  };
14042
- const req = provider !== undefined ? { ...base, provider } : base;
14142
+ let req = base;
14143
+ if (provider !== undefined)
14144
+ req = { ...req, provider };
14145
+ if (account !== undefined)
14146
+ req = { ...req, account };
14043
14147
  const data = await this.send(req);
14044
14148
  return data;
14045
14149
  }
@@ -16983,11 +16983,52 @@ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
16983
16983
  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."),
16984
16984
  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.")
16985
16985
  }).optional();
16986
+ var MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
16987
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
16988
+ }).transform((v) => v.trim().toLowerCase());
16989
+ var MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
16990
+ 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"
16991
+ });
16992
+ var MicrosoftAccountBindingSchema = exports_external.object({
16993
+ 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[]`."),
16994
+ 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."),
16995
+ org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 §6.4).")
16996
+ });
16986
16997
  var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
16987
- account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
16988
- message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
16989
- }).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)."),
16990
- 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).")
16998
+ 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)."),
16999
+ 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."),
17000
+ 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)."),
17001
+ 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.")
17002
+ }).superRefine((v, ctx) => {
17003
+ const hasSingular = v.account !== undefined;
17004
+ const hasPlural = v.accounts !== undefined;
17005
+ if (hasSingular && hasPlural) {
17006
+ ctx.addIssue({
17007
+ code: exports_external.ZodIssueCode.custom,
17008
+ message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
17009
+ path: ["accounts"]
17010
+ });
17011
+ }
17012
+ if (hasPlural && v.tools !== undefined) {
17013
+ ctx.addIssue({
17014
+ code: exports_external.ZodIssueCode.custom,
17015
+ message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
17016
+ path: ["tools"]
17017
+ });
17018
+ }
17019
+ if (hasPlural) {
17020
+ const seen = new Set;
17021
+ for (const b of v.accounts) {
17022
+ if (seen.has(b.account)) {
17023
+ ctx.addIssue({
17024
+ code: exports_external.ZodIssueCode.custom,
17025
+ message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
17026
+ path: ["accounts"]
17027
+ });
17028
+ }
17029
+ seen.add(b.account);
17030
+ }
17031
+ }
16991
17032
  }).optional();
16992
17033
  var AgentNotionWorkspaceConfigSchema = exports_external.object({
16993
17034
  databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
@@ -17362,6 +17403,26 @@ var SwitchroomConfigSchema = exports_external.object({
17362
17403
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
17363
17404
  checkServes(a.serves, ["agents", name, "serves"]);
17364
17405
  }
17406
+ const microsoftAccounts = cfg.microsoft_accounts;
17407
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
17408
+ const mw = a.microsoft_workspace;
17409
+ const accounts = mw?.accounts;
17410
+ if (!accounts)
17411
+ continue;
17412
+ accounts.forEach((b, i) => {
17413
+ const acct = b?.account?.trim().toLowerCase();
17414
+ if (!acct)
17415
+ return;
17416
+ const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
17417
+ if (!enabledFor.includes(name)) {
17418
+ ctx.addIssue({
17419
+ code: exports_external.ZodIssueCode.custom,
17420
+ 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}\``,
17421
+ path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
17422
+ });
17423
+ }
17424
+ });
17425
+ }
17365
17426
  });
17366
17427
 
17367
17428
  // src/config/paths.ts
@@ -17956,6 +18017,22 @@ function validateNotionWorkspaceConfig(config) {
17956
18017
  return issues;
17957
18018
  }
17958
18019
 
18020
+ // src/config/timezone.ts
18021
+ function isResolvableTimezone(zone) {
18022
+ try {
18023
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
18024
+ return true;
18025
+ } catch {
18026
+ return false;
18027
+ }
18028
+ }
18029
+ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
18030
+ "UTC",
18031
+ "Etc/UTC",
18032
+ "Etc/Universal",
18033
+ "Universal"
18034
+ ]);
18035
+
17959
18036
  // src/config/loader.ts
17960
18037
  class ConfigError extends Error {
17961
18038
  details;
@@ -18079,8 +18156,30 @@ function loadConfig(configPath) {
18079
18156
  if (notionIssues.length > 0) {
18080
18157
  throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
18081
18158
  }
18159
+ validateAllTimezones(config, filePath);
18082
18160
  return config;
18083
18161
  }
18162
+ function validateAllTimezones(config, filePath) {
18163
+ const issues = [];
18164
+ const check = (zone, where) => {
18165
+ if (zone == null)
18166
+ return;
18167
+ if (!isResolvableTimezone(zone)) {
18168
+ 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").`);
18169
+ }
18170
+ };
18171
+ check(config.switchroom?.timezone, "switchroom.timezone");
18172
+ check(config.defaults?.timezone, "defaults.timezone");
18173
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
18174
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
18175
+ }
18176
+ for (const [agentName, agentRaw] of Object.entries(config.agents)) {
18177
+ check(agentRaw?.timezone, `agents.${agentName}.timezone`);
18178
+ }
18179
+ if (issues.length > 0) {
18180
+ throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
18181
+ }
18182
+ }
18084
18183
  function validateAllCronTopicAliases(config, filePath) {
18085
18184
  const issues = [];
18086
18185
  for (const [agentName, agentRaw] of Object.entries(config.agents)) {
@@ -18179,14 +18278,6 @@ function allocateAgentUid(name) {
18179
18278
  return AGENT_UID_MIN + u32 % range;
18180
18279
  }
18181
18280
 
18182
- // src/config/timezone.ts
18183
- var CONTAINER_DEFAULT_UTC_ZONES = new Set([
18184
- "UTC",
18185
- "Etc/UTC",
18186
- "Etc/Universal",
18187
- "Universal"
18188
- ]);
18189
-
18190
18281
  // src/cli/agent-config.ts
18191
18282
  import { join } from "node:path";
18192
18283
  import { homedir as homedir2 } from "node:os";
@@ -18248,6 +18339,29 @@ var PROFILE_MEMORY_DEFAULTS = {
18248
18339
  }
18249
18340
  };
18250
18341
 
18342
+ // src/config/microsoft-workspace-acl.ts
18343
+ function normalizeMicrosoftBindings(mw) {
18344
+ if (!mw)
18345
+ return [];
18346
+ if (mw.accounts !== undefined) {
18347
+ return mw.accounts.filter((b) => b && typeof b.account === "string" && b.account.length > 0).map((b) => ({
18348
+ account: b.account.trim().toLowerCase(),
18349
+ tools: b.tools,
18350
+ org_mode: b.org_mode
18351
+ }));
18352
+ }
18353
+ if (typeof mw.account === "string" && mw.account.length > 0) {
18354
+ return [
18355
+ {
18356
+ account: mw.account.trim().toLowerCase(),
18357
+ tools: mw.tools,
18358
+ org_mode: mw.org_mode
18359
+ }
18360
+ ];
18361
+ }
18362
+ return [];
18363
+ }
18364
+
18251
18365
  // src/agents/reconcile-default-skills.ts
18252
18366
  var warnedMissingPool = new Set;
18253
18367
 
@@ -18605,6 +18719,12 @@ var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
18605
18719
  var DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin";
18606
18720
  var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
18607
18721
 
18722
+ // src/vault/grants-db-path.ts
18723
+ var GRANTS_DB_DIRNAME = "vault-broker";
18724
+ var GRANTS_DB_FILENAME = "vault-grants.db";
18725
+ var GRANTS_DB_CONTAINER_DIR = `/root/.switchroom/${GRANTS_DB_DIRNAME}`;
18726
+ var GRANTS_DB_CONTAINER_PATH = `${GRANTS_DB_CONTAINER_DIR}/${GRANTS_DB_FILENAME}`;
18727
+
18608
18728
  // src/agents/compose.ts
18609
18729
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
18610
18730
 
@@ -19909,7 +20029,8 @@ var GetCredentialsRequestSchema = exports_external.object({
19909
20029
  v: exports_external.literal(PROTOCOL_VERSION),
19910
20030
  op: exports_external.literal("get-credentials"),
19911
20031
  id: exports_external.string().min(1),
19912
- provider: ProviderNameSchema.optional()
20032
+ provider: ProviderNameSchema.optional(),
20033
+ account: exports_external.string().min(1).optional()
19913
20034
  });
19914
20035
  var ListStateRequestSchema = exports_external.object({
19915
20036
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -20599,7 +20720,7 @@ class AuthBroker {
20599
20720
  break;
20600
20721
  }
20601
20722
  if (provider === "microsoft") {
20602
- await this.opMicrosoftGetCredentials(socket, reqId, identity);
20723
+ await this.opMicrosoftGetCredentials(socket, reqId, identity, req.account);
20603
20724
  break;
20604
20725
  }
20605
20726
  socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in get-credentials dispatch`));
@@ -21698,14 +21819,31 @@ class AuthBroker {
21698
21819
  this.audit({ op: "rm-account", identity, account: label, accountKind: "google", ok: true });
21699
21820
  socket.write(encodeSuccess(id, { label }));
21700
21821
  }
21701
- async opMicrosoftGetCredentials(socket, id, identity) {
21822
+ async opMicrosoftGetCredentials(socket, id, identity, requestedAccount) {
21702
21823
  if (identity.kind !== "agent") {
21703
21824
  socket.write(encodeError(id, "INVALID_ARGS", `Microsoft get-credentials is per-agent only (caller kind '${identity.kind}' not supported); use the agent's per-agent socket bind`));
21704
21825
  return;
21705
21826
  }
21706
21827
  const agentName = identity.name;
21707
21828
  const agent = (this.config.agents ?? {})[agentName];
21708
- const account = agent?.microsoft_workspace?.account;
21829
+ const bindings = normalizeMicrosoftBindings(agent?.microsoft_workspace);
21830
+ let account;
21831
+ if (requestedAccount !== undefined) {
21832
+ const wanted = requestedAccount.trim().toLowerCase();
21833
+ const bound = bindings.find((b) => b.account === wanted);
21834
+ if (!bound) {
21835
+ this.audit({ op: "get-credentials", identity, account: wanted, accountKind: "microsoft", ok: false, error: "account-not-bound" });
21836
+ socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `agent '${agentName}' is not bound to Microsoft account '${wanted}' in switchroom.yaml microsoft_workspace`));
21837
+ return;
21838
+ }
21839
+ account = bound.account;
21840
+ } else if (bindings.length > 1) {
21841
+ this.audit({ op: "get-credentials", identity, accountKind: "microsoft", ok: false, error: "account-ambiguous" });
21842
+ socket.write(encodeError(id, "INVALID_ARGS", `agent '${agentName}' is bound to ${bindings.length} Microsoft accounts (${bindings.map((b) => b.account).join(", ")}); an account must be specified — call getCredentials("microsoft", <account>)`));
21843
+ return;
21844
+ } else {
21845
+ account = bindings[0]?.account;
21846
+ }
21709
21847
  if (!account) {
21710
21848
  this.audit({ op: "get-credentials", identity, accountKind: "microsoft", ok: false, error: "no-microsoft-account-configured" });
21711
21849
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `agent '${agentName}' has no microsoft_workspace.account configured in switchroom.yaml`));
@@ -4721,7 +4721,8 @@ var GetCredentialsRequestSchema = exports_external.object({
4721
4721
  v: exports_external.literal(PROTOCOL_VERSION),
4722
4722
  op: exports_external.literal("get-credentials"),
4723
4723
  id: exports_external.string().min(1),
4724
- provider: ProviderNameSchema.optional()
4724
+ provider: ProviderNameSchema.optional(),
4725
+ account: exports_external.string().min(1).optional()
4725
4726
  });
4726
4727
  var ListStateRequestSchema = exports_external.object({
4727
4728
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -5063,13 +5064,17 @@ class AuthBrokerClient {
5063
5064
  sock.destroy();
5064
5065
  }
5065
5066
  }
5066
- async getCredentials(provider) {
5067
+ async getCredentials(provider, account) {
5067
5068
  const base = {
5068
5069
  v: PROTOCOL_VERSION,
5069
5070
  id: randomUUID2(),
5070
5071
  op: "get-credentials"
5071
5072
  };
5072
- const req = provider !== undefined ? { ...base, provider } : base;
5073
+ let req = base;
5074
+ if (provider !== undefined)
5075
+ req = { ...req, provider };
5076
+ if (account !== undefined)
5077
+ req = { ...req, account };
5073
5078
  const data = await this.send(req);
5074
5079
  return data;
5075
5080
  }
@@ -4009,7 +4009,8 @@ var init_protocol = __esm(() => {
4009
4009
  v: exports_external.literal(PROTOCOL_VERSION),
4010
4010
  op: exports_external.literal("get-credentials"),
4011
4011
  id: exports_external.string().min(1),
4012
- provider: ProviderNameSchema.optional()
4012
+ provider: ProviderNameSchema.optional(),
4013
+ account: exports_external.string().min(1).optional()
4013
4014
  });
4014
4015
  ListStateRequestSchema = exports_external.object({
4015
4016
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -4324,13 +4325,17 @@ class AuthBrokerClient {
4324
4325
  sock.destroy();
4325
4326
  }
4326
4327
  }
4327
- async getCredentials(provider) {
4328
+ async getCredentials(provider, account) {
4328
4329
  const base = {
4329
4330
  v: PROTOCOL_VERSION,
4330
4331
  id: randomUUID(),
4331
4332
  op: "get-credentials"
4332
4333
  };
4333
- const req = provider !== undefined ? { ...base, provider } : base;
4334
+ let req = base;
4335
+ if (provider !== undefined)
4336
+ req = { ...req, provider };
4337
+ if (account !== undefined)
4338
+ req = { ...req, account };
4334
4339
  const data = await this.send(req);
4335
4340
  return data;
4336
4341
  }