switchroom 0.13.54 → 0.13.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +51 -0
- package/dist/auth-broker/index.js +51 -0
- package/dist/cli/ack-first-pretool.mjs +75 -0
- package/dist/cli/notion-write-pretool.mjs +13394 -0
- package/dist/cli/switchroom.js +1105 -375
- package/dist/host-control/main.js +51 -0
- package/dist/vault/approvals/kernel-server.js +52 -1
- package/dist/vault/broker/server.js +52 -1
- package/package.json +1 -1
- package/skills/notion/SKILL.md +148 -0
- package/telegram-plugin/ack-flag.ts +66 -0
- package/telegram-plugin/dist/gateway/gateway.js +413 -254
- package/telegram-plugin/gateway/gateway.ts +80 -1
- package/telegram-plugin/runtime-metrics.ts +17 -0
- package/telegram-plugin/silence-poke.ts +82 -0
- package/telegram-plugin/tests/ack-flag.test.ts +65 -0
- package/telegram-plugin/tests/post-fallback-outbound-count.test.ts +78 -0
- package/telegram-plugin/tests/silence-poke.test.ts +117 -7
|
@@ -13876,6 +13876,16 @@ var MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
|
13876
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."),
|
|
13877
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.")
|
|
13878
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();
|
|
13879
13889
|
var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
13880
13890
|
account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
13881
13891
|
message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
|
|
@@ -13889,6 +13899,13 @@ var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
|
13889
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)."),
|
|
13890
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).")
|
|
13891
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();
|
|
13892
13909
|
var ReactionsSchema = exports_external.object({
|
|
13893
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."),
|
|
13894
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`."),
|
|
@@ -14025,6 +14042,7 @@ var AgentSchema = exports_external.object({
|
|
|
14025
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."),
|
|
14026
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)."),
|
|
14027
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."),
|
|
14028
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({
|
|
14029
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."),
|
|
14030
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.")
|
|
@@ -14148,6 +14166,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
14148
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."),
|
|
14149
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)."),
|
|
14150
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."),
|
|
14151
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."),
|
|
14152
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)."),
|
|
14153
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)."),
|
|
@@ -14653,6 +14672,34 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
14653
14672
|
mergeAgentConfig.notifiedWorkerIsolationMove = false;
|
|
14654
14673
|
})(mergeAgentConfig ||= {});
|
|
14655
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
|
+
|
|
14656
14703
|
// src/config/loader.ts
|
|
14657
14704
|
class ConfigError extends Error {
|
|
14658
14705
|
details;
|
|
@@ -14772,6 +14819,10 @@ function loadConfig(configPath) {
|
|
|
14772
14819
|
}
|
|
14773
14820
|
applyAgentOverlays(config);
|
|
14774
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
|
+
}
|
|
14775
14826
|
return config;
|
|
14776
14827
|
}
|
|
14777
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({
|
|
@@ -11444,6 +11444,16 @@ var init_schema = __esm(() => {
|
|
|
11444
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."),
|
|
11445
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.")
|
|
11446
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();
|
|
11447
11457
|
AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
11448
11458
|
account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11449
11459
|
message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
|
|
@@ -11457,6 +11467,13 @@ var init_schema = __esm(() => {
|
|
|
11457
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)."),
|
|
11458
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).")
|
|
11459
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();
|
|
11460
11477
|
ReactionsSchema = exports_external.object({
|
|
11461
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."),
|
|
11462
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`."),
|
|
@@ -11593,6 +11610,7 @@ var init_schema = __esm(() => {
|
|
|
11593
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."),
|
|
11594
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)."),
|
|
11595
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."),
|
|
11596
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({
|
|
11597
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."),
|
|
11598
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.")
|
|
@@ -11716,6 +11734,7 @@ var init_schema = __esm(() => {
|
|
|
11716
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."),
|
|
11717
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)."),
|
|
11718
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."),
|
|
11719
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."),
|
|
11720
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)."),
|
|
11721
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)."),
|
|
@@ -11912,6 +11931,34 @@ var init_overlay_loader = __esm(() => {
|
|
|
11912
11931
|
OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
|
|
11913
11932
|
});
|
|
11914
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
|
+
|
|
11915
11962
|
// src/config/loader.ts
|
|
11916
11963
|
var exports_loader = {};
|
|
11917
11964
|
__export(exports_loader, {
|
|
@@ -12034,6 +12081,10 @@ function loadConfig(configPath) {
|
|
|
12034
12081
|
}
|
|
12035
12082
|
applyAgentOverlays(config);
|
|
12036
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
|
+
}
|
|
12037
12088
|
return config;
|
|
12038
12089
|
}
|
|
12039
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({
|
|
@@ -11444,6 +11444,16 @@ var init_schema = __esm(() => {
|
|
|
11444
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."),
|
|
11445
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.")
|
|
11446
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();
|
|
11447
11457
|
AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
11448
11458
|
account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11449
11459
|
message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
|
|
@@ -11457,6 +11467,13 @@ var init_schema = __esm(() => {
|
|
|
11457
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)."),
|
|
11458
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).")
|
|
11459
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();
|
|
11460
11477
|
ReactionsSchema = exports_external.object({
|
|
11461
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."),
|
|
11462
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`."),
|
|
@@ -11593,6 +11610,7 @@ var init_schema = __esm(() => {
|
|
|
11593
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."),
|
|
11594
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)."),
|
|
11595
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."),
|
|
11596
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({
|
|
11597
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."),
|
|
11598
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.")
|
|
@@ -11716,6 +11734,7 @@ var init_schema = __esm(() => {
|
|
|
11716
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."),
|
|
11717
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)."),
|
|
11718
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."),
|
|
11719
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."),
|
|
11720
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)."),
|
|
11721
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)."),
|
|
@@ -11912,6 +11931,34 @@ var init_overlay_loader = __esm(() => {
|
|
|
11912
11931
|
OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
|
|
11913
11932
|
});
|
|
11914
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
|
+
|
|
11915
11962
|
// src/config/loader.ts
|
|
11916
11963
|
var exports_loader = {};
|
|
11917
11964
|
__export(exports_loader, {
|
|
@@ -12034,6 +12081,10 @@ function loadConfig(configPath) {
|
|
|
12034
12081
|
}
|
|
12035
12082
|
applyAgentOverlays(config);
|
|
12036
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
|
+
}
|
|
12037
12088
|
return config;
|
|
12038
12089
|
}
|
|
12039
12090
|
function validateAllCronTopicAliases(config, filePath) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
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. **In switchroom v1, `search`
|
|
51
|
+
is BLOCKED for any agent with a non-empty
|
|
52
|
+
`notion_workspace.databases:` allowlist** — the post-filter that
|
|
53
|
+
would strip out-of-allowlist results isn't wired yet. Use
|
|
54
|
+
`query_database` against your allowed DBs instead. Admin-shaped
|
|
55
|
+
agents (no per-DB filter) can search normally.
|
|
56
|
+
- **Database queries.** `query_database` runs a filter+sort against
|
|
57
|
+
a database. You need the database's UUID (operator gives these
|
|
58
|
+
via friendly names in `notion_workspace.databases` — ask the
|
|
59
|
+
operator for "what's the UUID for X" if needed).
|
|
60
|
+
- **Page reads.** `get_page` and `get_block_children` walk a page's
|
|
61
|
+
structure. Both go through the allowlist gate.
|
|
62
|
+
- **Page writes.** `create_page` (with `parent.database_id`),
|
|
63
|
+
`update_page`, `update_block`, `append_block_children`,
|
|
64
|
+
`delete_block`, `create_comment`. The allowlist gate runs first;
|
|
65
|
+
blocked tool calls return a reason you can surface to the operator.
|
|
66
|
+
- **Database writes.** `update_database` — schema changes,
|
|
67
|
+
rename, etc. Gated.
|
|
68
|
+
|
|
69
|
+
## Tools NOT available
|
|
70
|
+
|
|
71
|
+
- **`create_database`** — disabled in v1 (operators create
|
|
72
|
+
databases via Notion's UI). If the user asks you to "create a new
|
|
73
|
+
database for X", say so and offer to populate an existing one or
|
|
74
|
+
prompt the operator to make the DB first.
|
|
75
|
+
- **`delete_database`** — same posture.
|
|
76
|
+
|
|
77
|
+
## Common workflows
|
|
78
|
+
|
|
79
|
+
### "What's on my tasks?"
|
|
80
|
+
|
|
81
|
+
1. Ask the operator (or use `config_get` to check your own
|
|
82
|
+
`notion_workspace.databases`) for the friendly name of the tasks
|
|
83
|
+
DB. The friendly name resolves to a UUID via
|
|
84
|
+
`notion_workspace.databases` in switchroom.yaml.
|
|
85
|
+
2. `query_database` with that UUID. Default filter: `not done`.
|
|
86
|
+
3. Format results as a brief markdown list. Don't dump the full
|
|
87
|
+
property soup — operators want titles + status, not raw IDs.
|
|
88
|
+
|
|
89
|
+
### "Add `<thing>` to Notion"
|
|
90
|
+
|
|
91
|
+
1. Default destination is the database whose friendly name matches
|
|
92
|
+
the user's intent (`tasks`, `notes`, `essays`). Ask if it's
|
|
93
|
+
ambiguous.
|
|
94
|
+
2. Use `create_page` with `parent.database_id: <uuid>`. The page's
|
|
95
|
+
properties need to match the target DB's schema — call
|
|
96
|
+
`retrieve_database` first if you're unsure of the property
|
|
97
|
+
names.
|
|
98
|
+
3. After creation, confirm to the user with the page title and
|
|
99
|
+
the Notion URL.
|
|
100
|
+
|
|
101
|
+
### "Find that thing about X"
|
|
102
|
+
|
|
103
|
+
1. If you have full search access (no `databases:` filter): `search`
|
|
104
|
+
with the query, take the top result unless ambiguous.
|
|
105
|
+
2. If `search` is blocked for you (the hook will return a clear
|
|
106
|
+
message naming the v1 limitation): `query_database` against
|
|
107
|
+
each of your allowed DBs in turn, with a title-contains or
|
|
108
|
+
property filter. List your allowed DBs by checking
|
|
109
|
+
`notion_workspace.databases:` in your config via `config_get`.
|
|
110
|
+
|
|
111
|
+
### "Update the page about X"
|
|
112
|
+
|
|
113
|
+
1. `search` or `query_database` to find the page.
|
|
114
|
+
2. `update_page` (for properties — status, tags, dates) or
|
|
115
|
+
`append_block_children` (for body content).
|
|
116
|
+
3. **Be careful** with `update_block` and `delete_block` — these
|
|
117
|
+
modify the page's body irreversibly. Prefer `append_block_children`
|
|
118
|
+
when the user's intent is "add a note to this page", not
|
|
119
|
+
"replace what's there".
|
|
120
|
+
|
|
121
|
+
## Limits and behaviours
|
|
122
|
+
|
|
123
|
+
- **Rate limit**: Notion's public API is ~3 rps per integration.
|
|
124
|
+
Multi-step turns with many writes may slow down (the hook makes
|
|
125
|
+
resolver calls per write). If you see "Notion API failed: 429",
|
|
126
|
+
back off and retry once.
|
|
127
|
+
- **No `create_database`**: if the user asks for a new database,
|
|
128
|
+
say "I can write into existing DBs but the operator creates new
|
|
129
|
+
ones in Notion's UI." Then offer the closest existing DB.
|
|
130
|
+
- **Standalone pages denied**: pages without a database parent
|
|
131
|
+
(workspace root pages, personal sub-pages) are hard-denied in
|
|
132
|
+
v1. The block reason names this when it fires; pass it through
|
|
133
|
+
to the user.
|
|
134
|
+
|
|
135
|
+
## When the allowlist denies you
|
|
136
|
+
|
|
137
|
+
If a tool call returns "DB <uuid> is not in your allowlist", that's
|
|
138
|
+
the operator's intended scope — don't try to work around it. Surface
|
|
139
|
+
the message to the user honestly: "I'm not configured to access that
|
|
140
|
+
database. You can add it to my allowlist with
|
|
141
|
+
`agents.<my-name>.notion_workspace.databases` in switchroom.yaml."
|
|
142
|
+
|
|
143
|
+
## Authoring small notes vs full pages
|
|
144
|
+
|
|
145
|
+
For one-line "log this thought" intents, prefer `append_block_children`
|
|
146
|
+
to an existing daily-notes / inbox page rather than creating a new
|
|
147
|
+
page per note. Operators usually have an inbox DB; ask if you're
|
|
148
|
+
unsure.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ack-first state flag — load-bearing for the ack-first-pretool hook
|
|
3
|
+
* (see `src/cli/ack-first-pretool.ts` and
|
|
4
|
+
* `reference/conversational-pacing.md` beat 1).
|
|
5
|
+
*
|
|
6
|
+
* The gateway is the source of truth for "has the model called reply
|
|
7
|
+
* yet this turn?". The PreToolUse hook runs as a short-lived child
|
|
8
|
+
* process so it can't read gateway in-memory state; the hand-off is
|
|
9
|
+
* a single file inside `$TELEGRAM_STATE_DIR`:
|
|
10
|
+
*
|
|
11
|
+
* - `markAckSent()` touches the file on the first reply per turn.
|
|
12
|
+
* - `clearAckSent()` removes it at turn_started.
|
|
13
|
+
* - The hook checks `existsSync(path)` → allow / block.
|
|
14
|
+
*
|
|
15
|
+
* Per-agent isolation is built-in: `$TELEGRAM_STATE_DIR` is the
|
|
16
|
+
* agent's per-container state dir (~/.switchroom/agents/<name>/telegram,
|
|
17
|
+
* bind-mounted into /state/agent/home/.switchroom/agents/<name>/telegram
|
|
18
|
+
* inside the container).
|
|
19
|
+
*
|
|
20
|
+
* All operations are best-effort: a write or unlink failure logs to
|
|
21
|
+
* stderr and returns; the gate is informational UX, not a safety
|
|
22
|
+
* primitive, so a broken state-dir must never wedge reply itself.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { closeSync, existsSync, openSync, unlinkSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
|
|
28
|
+
export const ACK_SENT_MARKER = "ack-sent.flag";
|
|
29
|
+
|
|
30
|
+
function markerPath(): string | null {
|
|
31
|
+
const dir = process.env.TELEGRAM_STATE_DIR;
|
|
32
|
+
if (!dir) return null;
|
|
33
|
+
return join(dir, ACK_SENT_MARKER);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Create the ack-sent marker. Idempotent (no-op if it exists). */
|
|
37
|
+
export function markAckSent(): void {
|
|
38
|
+
const path = markerPath();
|
|
39
|
+
if (path == null) return;
|
|
40
|
+
if (existsSync(path)) return;
|
|
41
|
+
try {
|
|
42
|
+
const fd = openSync(path, "w");
|
|
43
|
+
closeSync(fd);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`ack-flag: markAckSent failed path=${path}: ${err}\n`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Remove the ack-sent marker. Idempotent. */
|
|
52
|
+
export function clearAckSent(): void {
|
|
53
|
+
const path = markerPath();
|
|
54
|
+
if (path == null) return;
|
|
55
|
+
try {
|
|
56
|
+
unlinkSync(path);
|
|
57
|
+
} catch (err: unknown) {
|
|
58
|
+
// ENOENT is the common case (turn started before any prior turn
|
|
59
|
+
// ran a reply); silently swallow.
|
|
60
|
+
const code = (err as { code?: string } | undefined)?.code;
|
|
61
|
+
if (code === "ENOENT") return;
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
`ack-flag: clearAckSent failed path=${path}: ${err}\n`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|