switchroom 0.13.53 → 0.13.55

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.
@@ -13732,7 +13732,8 @@ var AgentMemorySchema = exports_external.object({
13732
13732
  recall: exports_external.object({
13733
13733
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
13734
13734
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
13735
- min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`.")
13735
+ min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
13736
+ topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
13736
13737
  }).optional().describe("Auto-recall tuning knobs")
13737
13738
  }).optional();
13738
13739
  var HookEntrySchema = exports_external.object({
@@ -13875,6 +13876,16 @@ var MicrosoftWorkspaceConfigSchema = exports_external.object({
13875
13876
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
13876
13877
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
13877
13878
  }).optional();
13879
+ var NotionWorkspaceConfigSchema = exports_external.object({
13880
+ vault_key: exports_external.string().min(1).default("notion/integration-token").describe("Vault key holding the Notion internal-integration token. Default " + "`notion/integration-token`. Override only for non-standard vault " + "layouts. The broker's --allow ACL on this key is the authoritative " + "list of which agents may receive the token."),
13881
+ databases: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
13882
+ message: "notion_workspace.databases friendly names must match " + "/^[a-z0-9][a-z0-9_-]{0,62}$/ — lowercase letters, digits, " + "hyphens, underscores. Got: '%s'."
13883
+ }), exports_external.string().regex(/^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/, {
13884
+ message: "notion_workspace.databases values must be Notion database " + "UUIDs (32 hex characters, optional dashes)."
13885
+ })).default({}).describe("Friendly-name → Notion database UUID map. Operator-managed; agents " + "reference databases by friendly name only — they never see or type " + "UUIDs. Populate via `switchroom notion list-dbs` (PR 4) after " + "vault-putting the integration token and sharing DBs with the " + "integration in Notion's UI."),
13886
+ mcp_version: exports_external.string().min(1).optional().describe("Optional pin for the upstream `@notionhq/notion-mcp-server` npm " + "package version. Default is the build-time `NOTION_MCP_PINNED_VERSION` " + "constant. Override only when reproducing operator-specific bugs."),
13887
+ rate_limit_rps: exports_external.number().int().positive().max(10).optional().describe("Optional global rate-limit budget in requests per second across all " + "switchroom agents sharing this integration token. Defaults to 3 " + "(Notion's documented public-API limit). Lower it if you also use " + "the integration token from outside switchroom and need to share " + "budget. Higher than 10 is rejected — if you think you need it, " + "your usage probably needs a workspace-tier upgrade with Notion.")
13888
+ }).optional();
13878
13889
  var AgentGoogleWorkspaceConfigSchema = exports_external.object({
13879
13890
  account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
13880
13891
  message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
@@ -13888,6 +13899,13 @@ var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
13888
13899
  }).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)."),
13889
13900
  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).")
13890
13901
  }).optional();
13902
+ var AgentNotionWorkspaceConfigSchema = exports_external.object({
13903
+ databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
13904
+ message: "notion_workspace.databases entries must be friendly names " + "matching /^[a-z0-9][a-z0-9_-]{0,62}$/ — these must appear as " + "keys in top-level notion_workspace.databases."
13905
+ })).min(1, {
13906
+ message: "notion_workspace.databases must list at least one friendly " + "name. An empty list rejects every Notion tool call — if you " + "want to remove this agent's Notion access, delete the entire " + "notion_workspace block instead."
13907
+ }).optional().describe("Optional per-agent allowlist of database friendly names this " + "agent may read/write. Each name must exist as a key in top-level " + "notion_workspace.databases. Omit the field (or leave it undefined) " + "to grant access to every DB the upstream integration can see — " + "appropriate for an admin/orchestrator agent. Set the list to " + "narrow access for specialist agents.")
13908
+ }).optional();
13891
13909
  var ReactionsSchema = exports_external.object({
13892
13910
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
13893
13911
  trigger_emojis: exports_external.array(exports_external.string()).optional().describe("Emoji allowlist that triggers a synthetic inbound when reacted " + "to a bot message. Default ['\uD83D\uDC4E', '❌', '\uD83D\uDC4D', '✅']. Cascade " + "mode: REPLACE (not union) — setting this at a layer replaces " + "lower layers entirely, so an operator can narrow to [] to " + "disable triggering without flipping `enabled`."),
@@ -14024,6 +14042,7 @@ var AgentSchema = exports_external.object({
14024
14042
  drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
14025
14043
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
14026
14044
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
14045
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
14027
14046
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
14028
14047
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
14029
14048
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
@@ -14147,6 +14166,7 @@ var SwitchroomConfigSchema = exports_external.object({
14147
14166
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
14148
14167
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
14149
14168
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
14169
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
14150
14170
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
14151
14171
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
14152
14172
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
@@ -14652,6 +14672,34 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14652
14672
  mergeAgentConfig.notifiedWorkerIsolationMove = false;
14653
14673
  })(mergeAgentConfig ||= {});
14654
14674
 
14675
+ // src/config/notion-workspace-acl.ts
14676
+ function validateNotionWorkspaceConfig(config) {
14677
+ const issues = [];
14678
+ const dbMap = config.notion_workspace?.databases ?? {};
14679
+ const known = new Set(Object.keys(dbMap));
14680
+ for (const [agentName, agentRaw] of Object.entries(config.agents ?? {})) {
14681
+ if (!agentRaw)
14682
+ continue;
14683
+ const dbFilter = agentRaw.notion_workspace?.databases;
14684
+ if (dbFilter === undefined)
14685
+ continue;
14686
+ if (dbFilter.length === 0) {
14687
+ issues.push(` agents.${agentName}.notion_workspace.databases is an empty ` + `list. Delete the entire notion_workspace block to remove Notion ` + `access, or list at least one database friendly name.`);
14688
+ continue;
14689
+ }
14690
+ if (config.notion_workspace === undefined) {
14691
+ issues.push(` agents.${agentName}.notion_workspace is set but the top-level ` + `notion_workspace block is missing. Configure the integration ` + `globally first (vault_key + databases map), then grant per-agent ` + `access.`);
14692
+ continue;
14693
+ }
14694
+ for (const name of dbFilter) {
14695
+ if (!known.has(name)) {
14696
+ issues.push(` agents.${agentName}.notion_workspace.databases references ` + `unknown database "${name}". Add it to top-level ` + `notion_workspace.databases (run \`switchroom notion list-dbs\` ` + `to see the UUIDs the integration can read), or remove the ` + `reference.`);
14697
+ }
14698
+ }
14699
+ }
14700
+ return issues;
14701
+ }
14702
+
14655
14703
  // src/config/loader.ts
14656
14704
  class ConfigError extends Error {
14657
14705
  details;
@@ -14771,6 +14819,10 @@ function loadConfig(configPath) {
14771
14819
  }
14772
14820
  applyAgentOverlays(config);
14773
14821
  validateAllCronTopicAliases(config, filePath);
14822
+ const notionIssues = validateNotionWorkspaceConfig(config);
14823
+ if (notionIssues.length > 0) {
14824
+ throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
14825
+ }
14774
14826
  return config;
14775
14827
  }
14776
14828
  function validateAllCronTopicAliases(config, filePath) {
@@ -11259,7 +11259,7 @@ var init_dist = __esm(() => {
11259
11259
  });
11260
11260
 
11261
11261
  // src/config/schema.ts
11262
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11262
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11263
11263
  var init_schema = __esm(() => {
11264
11264
  init_zod();
11265
11265
  CodeRepoEntrySchema = exports_external.object({
@@ -11300,7 +11300,8 @@ var init_schema = __esm(() => {
11300
11300
  recall: exports_external.object({
11301
11301
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
11302
11302
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
11303
- min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`.")
11303
+ min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
11304
+ topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
11304
11305
  }).optional().describe("Auto-recall tuning knobs")
11305
11306
  }).optional();
11306
11307
  HookEntrySchema = exports_external.object({
@@ -11443,6 +11444,16 @@ var init_schema = __esm(() => {
11443
11444
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
11444
11445
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
11445
11446
  }).optional();
11447
+ NotionWorkspaceConfigSchema = exports_external.object({
11448
+ vault_key: exports_external.string().min(1).default("notion/integration-token").describe("Vault key holding the Notion internal-integration token. Default " + "`notion/integration-token`. Override only for non-standard vault " + "layouts. The broker's --allow ACL on this key is the authoritative " + "list of which agents may receive the token."),
11449
+ databases: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
11450
+ message: "notion_workspace.databases friendly names must match " + "/^[a-z0-9][a-z0-9_-]{0,62}$/ — lowercase letters, digits, " + "hyphens, underscores. Got: '%s'."
11451
+ }), exports_external.string().regex(/^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/, {
11452
+ message: "notion_workspace.databases values must be Notion database " + "UUIDs (32 hex characters, optional dashes)."
11453
+ })).default({}).describe("Friendly-name → Notion database UUID map. Operator-managed; agents " + "reference databases by friendly name only — they never see or type " + "UUIDs. Populate via `switchroom notion list-dbs` (PR 4) after " + "vault-putting the integration token and sharing DBs with the " + "integration in Notion's UI."),
11454
+ mcp_version: exports_external.string().min(1).optional().describe("Optional pin for the upstream `@notionhq/notion-mcp-server` npm " + "package version. Default is the build-time `NOTION_MCP_PINNED_VERSION` " + "constant. Override only when reproducing operator-specific bugs."),
11455
+ rate_limit_rps: exports_external.number().int().positive().max(10).optional().describe("Optional global rate-limit budget in requests per second across all " + "switchroom agents sharing this integration token. Defaults to 3 " + "(Notion's documented public-API limit). Lower it if you also use " + "the integration token from outside switchroom and need to share " + "budget. Higher than 10 is rejected — if you think you need it, " + "your usage probably needs a workspace-tier upgrade with Notion.")
11456
+ }).optional();
11446
11457
  AgentGoogleWorkspaceConfigSchema = exports_external.object({
11447
11458
  account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11448
11459
  message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
@@ -11456,6 +11467,13 @@ var init_schema = __esm(() => {
11456
11467
  }).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)."),
11457
11468
  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).")
11458
11469
  }).optional();
11470
+ AgentNotionWorkspaceConfigSchema = exports_external.object({
11471
+ databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
11472
+ message: "notion_workspace.databases entries must be friendly names " + "matching /^[a-z0-9][a-z0-9_-]{0,62}$/ — these must appear as " + "keys in top-level notion_workspace.databases."
11473
+ })).min(1, {
11474
+ message: "notion_workspace.databases must list at least one friendly " + "name. An empty list rejects every Notion tool call — if you " + "want to remove this agent's Notion access, delete the entire " + "notion_workspace block instead."
11475
+ }).optional().describe("Optional per-agent allowlist of database friendly names this " + "agent may read/write. Each name must exist as a key in top-level " + "notion_workspace.databases. Omit the field (or leave it undefined) " + "to grant access to every DB the upstream integration can see — " + "appropriate for an admin/orchestrator agent. Set the list to " + "narrow access for specialist agents.")
11476
+ }).optional();
11459
11477
  ReactionsSchema = exports_external.object({
11460
11478
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
11461
11479
  trigger_emojis: exports_external.array(exports_external.string()).optional().describe("Emoji allowlist that triggers a synthetic inbound when reacted " + "to a bot message. Default ['\uD83D\uDC4E', '❌', '\uD83D\uDC4D', '✅']. Cascade " + "mode: REPLACE (not union) — setting this at a layer replaces " + "lower layers entirely, so an operator can narrow to [] to " + "disable triggering without flipping `enabled`."),
@@ -11592,6 +11610,7 @@ var init_schema = __esm(() => {
11592
11610
  drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
11593
11611
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
11594
11612
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
11613
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
11595
11614
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
11596
11615
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
11597
11616
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
@@ -11715,6 +11734,7 @@ var init_schema = __esm(() => {
11715
11734
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
11716
11735
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11717
11736
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
11737
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
11718
11738
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11719
11739
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11720
11740
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
@@ -11911,6 +11931,34 @@ var init_overlay_loader = __esm(() => {
11911
11931
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
11912
11932
  });
11913
11933
 
11934
+ // src/config/notion-workspace-acl.ts
11935
+ function validateNotionWorkspaceConfig(config) {
11936
+ const issues = [];
11937
+ const dbMap = config.notion_workspace?.databases ?? {};
11938
+ const known = new Set(Object.keys(dbMap));
11939
+ for (const [agentName, agentRaw] of Object.entries(config.agents ?? {})) {
11940
+ if (!agentRaw)
11941
+ continue;
11942
+ const dbFilter = agentRaw.notion_workspace?.databases;
11943
+ if (dbFilter === undefined)
11944
+ continue;
11945
+ if (dbFilter.length === 0) {
11946
+ issues.push(` agents.${agentName}.notion_workspace.databases is an empty ` + `list. Delete the entire notion_workspace block to remove Notion ` + `access, or list at least one database friendly name.`);
11947
+ continue;
11948
+ }
11949
+ if (config.notion_workspace === undefined) {
11950
+ issues.push(` agents.${agentName}.notion_workspace is set but the top-level ` + `notion_workspace block is missing. Configure the integration ` + `globally first (vault_key + databases map), then grant per-agent ` + `access.`);
11951
+ continue;
11952
+ }
11953
+ for (const name of dbFilter) {
11954
+ if (!known.has(name)) {
11955
+ issues.push(` agents.${agentName}.notion_workspace.databases references ` + `unknown database "${name}". Add it to top-level ` + `notion_workspace.databases (run \`switchroom notion list-dbs\` ` + `to see the UUIDs the integration can read), or remove the ` + `reference.`);
11956
+ }
11957
+ }
11958
+ }
11959
+ return issues;
11960
+ }
11961
+
11914
11962
  // src/config/loader.ts
11915
11963
  var exports_loader = {};
11916
11964
  __export(exports_loader, {
@@ -12033,6 +12081,10 @@ function loadConfig(configPath) {
12033
12081
  }
12034
12082
  applyAgentOverlays(config);
12035
12083
  validateAllCronTopicAliases(config, filePath);
12084
+ const notionIssues = validateNotionWorkspaceConfig(config);
12085
+ if (notionIssues.length > 0) {
12086
+ throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
12087
+ }
12036
12088
  return config;
12037
12089
  }
12038
12090
  function validateAllCronTopicAliases(config, filePath) {
@@ -11259,7 +11259,7 @@ var init_zod = __esm(() => {
11259
11259
  });
11260
11260
 
11261
11261
  // src/config/schema.ts
11262
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11262
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
11263
11263
  var init_schema = __esm(() => {
11264
11264
  init_zod();
11265
11265
  CodeRepoEntrySchema = exports_external.object({
@@ -11300,7 +11300,8 @@ var init_schema = __esm(() => {
11300
11300
  recall: exports_external.object({
11301
11301
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
11302
11302
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
11303
- min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`.")
11303
+ min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default — " + "current behaviour). Try 0.10–0.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
11304
+ topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
11304
11305
  }).optional().describe("Auto-recall tuning knobs")
11305
11306
  }).optional();
11306
11307
  HookEntrySchema = exports_external.object({
@@ -11443,6 +11444,16 @@ var init_schema = __esm(() => {
11443
11444
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
11444
11445
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
11445
11446
  }).optional();
11447
+ NotionWorkspaceConfigSchema = exports_external.object({
11448
+ vault_key: exports_external.string().min(1).default("notion/integration-token").describe("Vault key holding the Notion internal-integration token. Default " + "`notion/integration-token`. Override only for non-standard vault " + "layouts. The broker's --allow ACL on this key is the authoritative " + "list of which agents may receive the token."),
11449
+ databases: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
11450
+ message: "notion_workspace.databases friendly names must match " + "/^[a-z0-9][a-z0-9_-]{0,62}$/ — lowercase letters, digits, " + "hyphens, underscores. Got: '%s'."
11451
+ }), exports_external.string().regex(/^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/, {
11452
+ message: "notion_workspace.databases values must be Notion database " + "UUIDs (32 hex characters, optional dashes)."
11453
+ })).default({}).describe("Friendly-name → Notion database UUID map. Operator-managed; agents " + "reference databases by friendly name only — they never see or type " + "UUIDs. Populate via `switchroom notion list-dbs` (PR 4) after " + "vault-putting the integration token and sharing DBs with the " + "integration in Notion's UI."),
11454
+ mcp_version: exports_external.string().min(1).optional().describe("Optional pin for the upstream `@notionhq/notion-mcp-server` npm " + "package version. Default is the build-time `NOTION_MCP_PINNED_VERSION` " + "constant. Override only when reproducing operator-specific bugs."),
11455
+ rate_limit_rps: exports_external.number().int().positive().max(10).optional().describe("Optional global rate-limit budget in requests per second across all " + "switchroom agents sharing this integration token. Defaults to 3 " + "(Notion's documented public-API limit). Lower it if you also use " + "the integration token from outside switchroom and need to share " + "budget. Higher than 10 is rejected — if you think you need it, " + "your usage probably needs a workspace-tier upgrade with Notion.")
11456
+ }).optional();
11446
11457
  AgentGoogleWorkspaceConfigSchema = exports_external.object({
11447
11458
  account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11448
11459
  message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
@@ -11456,6 +11467,13 @@ var init_schema = __esm(() => {
11456
11467
  }).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)."),
11457
11468
  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).")
11458
11469
  }).optional();
11470
+ AgentNotionWorkspaceConfigSchema = exports_external.object({
11471
+ databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
11472
+ message: "notion_workspace.databases entries must be friendly names " + "matching /^[a-z0-9][a-z0-9_-]{0,62}$/ — these must appear as " + "keys in top-level notion_workspace.databases."
11473
+ })).min(1, {
11474
+ message: "notion_workspace.databases must list at least one friendly " + "name. An empty list rejects every Notion tool call — if you " + "want to remove this agent's Notion access, delete the entire " + "notion_workspace block instead."
11475
+ }).optional().describe("Optional per-agent allowlist of database friendly names this " + "agent may read/write. Each name must exist as a key in top-level " + "notion_workspace.databases. Omit the field (or leave it undefined) " + "to grant access to every DB the upstream integration can see — " + "appropriate for an admin/orchestrator agent. Set the list to " + "narrow access for specialist agents.")
11476
+ }).optional();
11459
11477
  ReactionsSchema = exports_external.object({
11460
11478
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
11461
11479
  trigger_emojis: exports_external.array(exports_external.string()).optional().describe("Emoji allowlist that triggers a synthetic inbound when reacted " + "to a bot message. Default ['\uD83D\uDC4E', '❌', '\uD83D\uDC4D', '✅']. Cascade " + "mode: REPLACE (not union) — setting this at a layer replaces " + "lower layers entirely, so an operator can narrow to [] to " + "disable triggering without flipping `enabled`."),
@@ -11592,6 +11610,7 @@ var init_schema = __esm(() => {
11592
11610
  drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
11593
11611
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
11594
11612
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
11613
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
11595
11614
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
11596
11615
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
11597
11616
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
@@ -11715,6 +11734,7 @@ var init_schema = __esm(() => {
11715
11734
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
11716
11735
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11717
11736
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
11737
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
11718
11738
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11719
11739
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11720
11740
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
@@ -11911,6 +11931,34 @@ var init_overlay_loader = __esm(() => {
11911
11931
  OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
11912
11932
  });
11913
11933
 
11934
+ // src/config/notion-workspace-acl.ts
11935
+ function validateNotionWorkspaceConfig(config) {
11936
+ const issues = [];
11937
+ const dbMap = config.notion_workspace?.databases ?? {};
11938
+ const known = new Set(Object.keys(dbMap));
11939
+ for (const [agentName, agentRaw] of Object.entries(config.agents ?? {})) {
11940
+ if (!agentRaw)
11941
+ continue;
11942
+ const dbFilter = agentRaw.notion_workspace?.databases;
11943
+ if (dbFilter === undefined)
11944
+ continue;
11945
+ if (dbFilter.length === 0) {
11946
+ issues.push(` agents.${agentName}.notion_workspace.databases is an empty ` + `list. Delete the entire notion_workspace block to remove Notion ` + `access, or list at least one database friendly name.`);
11947
+ continue;
11948
+ }
11949
+ if (config.notion_workspace === undefined) {
11950
+ issues.push(` agents.${agentName}.notion_workspace is set but the top-level ` + `notion_workspace block is missing. Configure the integration ` + `globally first (vault_key + databases map), then grant per-agent ` + `access.`);
11951
+ continue;
11952
+ }
11953
+ for (const name of dbFilter) {
11954
+ if (!known.has(name)) {
11955
+ issues.push(` agents.${agentName}.notion_workspace.databases references ` + `unknown database "${name}". Add it to top-level ` + `notion_workspace.databases (run \`switchroom notion list-dbs\` ` + `to see the UUIDs the integration can read), or remove the ` + `reference.`);
11956
+ }
11957
+ }
11958
+ }
11959
+ return issues;
11960
+ }
11961
+
11914
11962
  // src/config/loader.ts
11915
11963
  var exports_loader = {};
11916
11964
  __export(exports_loader, {
@@ -12033,6 +12081,10 @@ function loadConfig(configPath) {
12033
12081
  }
12034
12082
  applyAgentOverlays(config);
12035
12083
  validateAllCronTopicAliases(config, filePath);
12084
+ const notionIssues = validateNotionWorkspaceConfig(config);
12085
+ if (notionIssues.length > 0) {
12086
+ throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
12087
+ }
12036
12088
  return config;
12037
12089
  }
12038
12090
  function validateAllCronTopicAliases(config, filePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.53",
3
+ "version": "0.13.55",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -239,6 +239,23 @@ export HINDSIGHT_RECALL_CACHE_TTL_SECS={{hindsightRecallCacheTtlSecs}}
239
239
  {{#if (isNumber hindsightRecallMinOverlap)}}
240
240
  export HINDSIGHT_RECALL_MIN_OVERLAP={{hindsightRecallMinOverlap}}
241
241
  {{/if}}
242
+ # PR6 — supergroup-mode topic tagging. JSON map of {alias: thread_id}
243
+ # parsed by retain.py + recall.py to (a) stamp chat_id/thread_id/topic_alias
244
+ # into retained memory metadata and (b) emit a "Current topic: …" preamble
245
+ # on recall blocks so the model self-scopes. Empty / absent for fleet-shared
246
+ # or DM agents where the supergroup topology isn't in use.
247
+ {{#if hindsightTopicAliasesJsonQ}}
248
+ export HINDSIGHT_TOPIC_ALIASES_JSON={{{hindsightTopicAliasesJsonQ}}}
249
+ {{/if}}
250
+ # PR6 — topic filter mode for cross-topic memory recall. Default
251
+ # "soft-preamble": all topic-tagged memories surface and the model
252
+ # decides relevance via the preamble. "hard-filter": drop memories
253
+ # whose source thread_id differs from the active prompt's thread_id.
254
+ # Operators flip this when instrumentation (the active_thread_id /
255
+ # source_topics fields in recall_log.jsonl) shows binding failures.
256
+ {{#if hindsightTopicFilterMode}}
257
+ export HINDSIGHT_TOPIC_FILTER_MODE={{hindsightTopicFilterMode}}
258
+ {{/if}}
242
259
  # Wait for Hindsight API to be reachable before launching Claude, otherwise
243
260
  # the MCP server connection fails at startup with "1 MCP server failed".
244
261
  HINDSIGHT_WAIT=0
@@ -21,6 +21,8 @@ The 👀→🤔→🔥→👍 status reaction and the typing indicator are *ambi
21
21
 
22
22
  **Reactions ON your replies.** Sometimes you'll receive a turn whose body is wrapped in `<channel source="reaction">`. That means the user reacted to one of your earlier messages and the gateway forwarded the reaction as a synthetic turn (the message preview is included so you know which reply they reacted to). 👎 / ❌ are stop signals — pause, reconsider the approach, ask what's off. 👍 / ✅ are acknowledgements — keep going if mid-task, no extra reply needed. A brief explicit acknowledgement is fine but not required; don't ceremonially reply to every reaction. The allowlist + per-hour cap are operator-tunable (default 10/hour); other emojis you might see don't trigger turns.
23
23
 
24
+ **Topics are organizational, you are one identity.** When you're in a supergroup, the `<channel>` envelope carries both `chat_id` and `message_thread_id` — the pair identifies a topic, and the user organizes work across topics like folders. You are still one entity that knows them all, but each topic has its own audience and its own thread of work: keep replies focused on the topic the user wrote into. Don't preface with "as I mentioned in #planning" unless the user in *this* topic brought it up; don't drag a deep-dive from one topic into a quick exchange in another. Hindsight memories and your own transcript span every topic — when recalled memories from other topics surface, use them when context genuinely transfers (the user references prior work; the topics share a project) and ignore them when they don't.
25
+
24
26
  **Follow-ups while a turn is in flight.** Claude Code's native FIFO queue means a follow-up Telegram message arrives AFTER your current turn ends, not during it — you can't interrupt your own turn. Every follow-up becomes the next prompt you see. The plugin enriches the `<channel>` meta so you can classify correctly:
25
27
 
26
28
  - `queued="true"` — DEFAULT for mid-turn follow-ups (no prefix). Treat as a new, independent task. Do NOT reference the in-flight work — start fresh. Also fires when the user typed `/queue ` or `/q ` (legacy alias; the prefix is stripped from the body you see).
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: notion
3
+ version: 0.1.0
4
+ description: |
5
+ Use when the user wants to read, search, write, or update content in
6
+ their Notion workspace. The agent has access via the `notion` MCP
7
+ server (`@notionhq/notion-mcp-server`) configured by the operator
8
+ with a per-database allowlist.
9
+
10
+ Triggers on phrasings including: "add this to Notion", "what's on my
11
+ Notion tasks", "find that page about X in Notion", "create a Notion
12
+ page for this", "update the notion entry for X", "search Notion
13
+ for…", "append this to my Notion notes", "show me what's in the
14
+ essays database", "log this in Notion".
15
+
16
+ Do NOT use to create new Notion databases — that's disabled in v1
17
+ (operators create DBs via Notion's UI). Do NOT use to scan or
18
+ enumerate the operator's whole workspace ad-hoc — the per-database
19
+ allowlist scopes what the agent can see, and brute-forcing past it
20
+ is a defence-in-depth violation.
21
+
22
+ Also do not use this skill to file bugs against a GitHub repo
23
+ (that's `file-bug`) or to search the web (Notion search is workspace-
24
+ scoped only).
25
+ ---
26
+
27
+ # Notion
28
+
29
+ This skill is your interface to the operator's Notion workspace. The
30
+ operator has registered an integration in Notion's settings and
31
+ shared specific pages/databases with it. Your access is gated at two
32
+ layers:
33
+
34
+ 1. **Notion's upstream share-list** — the integration can only see
35
+ pages and databases the operator explicitly shared with it in
36
+ Notion's UI.
37
+ 2. **switchroom's per-agent allowlist** — within what the upstream
38
+ integration can see, the operator can further restrict YOU to a
39
+ subset via `agents.<your-name>.notion_workspace.databases:`.
40
+
41
+ Both layers are enforced at the broker / hook level. You don't have
42
+ to compute the intersection — if you call a tool against a
43
+ disallowed database, you'll get a clean block reason naming the DB.
44
+
45
+ ## Tool surface
46
+
47
+ The Notion MCP exposes the standard set:
48
+
49
+ - **Search.** `search` finds pages or databases by title/content
50
+ across what the integration can see. switchroom's PreToolUse hook
51
+ post-filters results to your allowlist — pages outside it are
52
+ dropped before they reach you. The response's metadata field
53
+ indicates how many were filtered.
54
+ - **Database queries.** `query_database` runs a filter+sort against
55
+ a database. You need the database's UUID (operator gives these
56
+ via friendly names in `notion_workspace.databases` — ask the
57
+ operator for "what's the UUID for X" if needed).
58
+ - **Page reads.** `get_page` and `get_block_children` walk a page's
59
+ structure. Both go through the allowlist gate.
60
+ - **Page writes.** `create_page` (with `parent.database_id`),
61
+ `update_page`, `update_block`, `append_block_children`,
62
+ `delete_block`, `create_comment`. The allowlist gate runs first;
63
+ blocked tool calls return a reason you can surface to the operator.
64
+ - **Database writes.** `update_database` — schema changes,
65
+ rename, etc. Gated.
66
+
67
+ ## Tools NOT available
68
+
69
+ - **`create_database`** — disabled in v1 (operators create
70
+ databases via Notion's UI). If the user asks you to "create a new
71
+ database for X", say so and offer to populate an existing one or
72
+ prompt the operator to make the DB first.
73
+ - **`delete_database`** — same posture.
74
+
75
+ ## Common workflows
76
+
77
+ ### "What's on my tasks?"
78
+
79
+ 1. Ask the operator (or use `config_get` to check your own
80
+ `notion_workspace.databases`) for the friendly name of the tasks
81
+ DB. The friendly name resolves to a UUID via
82
+ `notion_workspace.databases` in switchroom.yaml.
83
+ 2. `query_database` with that UUID. Default filter: `not done`.
84
+ 3. Format results as a brief markdown list. Don't dump the full
85
+ property soup — operators want titles + status, not raw IDs.
86
+
87
+ ### "Add `<thing>` to Notion"
88
+
89
+ 1. Default destination is the database whose friendly name matches
90
+ the user's intent (`tasks`, `notes`, `essays`). Ask if it's
91
+ ambiguous.
92
+ 2. Use `create_page` with `parent.database_id: <uuid>`. The page's
93
+ properties need to match the target DB's schema — call
94
+ `retrieve_database` first if you're unsure of the property
95
+ names.
96
+ 3. After creation, confirm to the user with the page title and
97
+ the Notion URL.
98
+
99
+ ### "Find that thing about X"
100
+
101
+ 1. `search` with the query. Take only the top result unless ambiguous.
102
+ 2. The post-filter has already redacted out-of-allowlist results;
103
+ don't worry about leakage.
104
+ 3. If 0 results: try a `query_database` against the most likely DB
105
+ with a property filter (matches the value).
106
+
107
+ ### "Update the page about X"
108
+
109
+ 1. `search` or `query_database` to find the page.
110
+ 2. `update_page` (for properties — status, tags, dates) or
111
+ `append_block_children` (for body content).
112
+ 3. **Be careful** with `update_block` and `delete_block` — these
113
+ modify the page's body irreversibly. Prefer `append_block_children`
114
+ when the user's intent is "add a note to this page", not
115
+ "replace what's there".
116
+
117
+ ## Limits and behaviours
118
+
119
+ - **Rate limit**: Notion's public API is ~3 rps per integration.
120
+ Multi-step turns with many writes may slow down (the hook makes
121
+ resolver calls per write). If you see "Notion API failed: 429",
122
+ back off and retry once.
123
+ - **No `create_database`**: if the user asks for a new database,
124
+ say "I can write into existing DBs but the operator creates new
125
+ ones in Notion's UI." Then offer the closest existing DB.
126
+ - **Standalone pages denied**: pages without a database parent
127
+ (workspace root pages, personal sub-pages) are hard-denied in
128
+ v1. The block reason names this when it fires; pass it through
129
+ to the user.
130
+
131
+ ## When the allowlist denies you
132
+
133
+ If a tool call returns "DB <uuid> is not in your allowlist", that's
134
+ the operator's intended scope — don't try to work around it. Surface
135
+ the message to the user honestly: "I'm not configured to access that
136
+ database. You can add it to my allowlist with
137
+ `agents.<my-name>.notion_workspace.databases` in switchroom.yaml."
138
+
139
+ ## Authoring small notes vs full pages
140
+
141
+ For one-line "log this thought" intents, prefer `append_block_children`
142
+ to an existing daily-notes / inbox page rather than creating a new
143
+ page per note. Operators usually have an inbox DB; ask if you're
144
+ unsure.