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
|
@@ -11141,6 +11141,16 @@ var MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
|
11141
11141
|
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."),
|
|
11142
11142
|
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.")
|
|
11143
11143
|
}).optional();
|
|
11144
|
+
var NotionWorkspaceConfigSchema = exports_external.object({
|
|
11145
|
+
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."),
|
|
11146
|
+
databases: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
|
|
11147
|
+
message: "notion_workspace.databases friendly names must match " + "/^[a-z0-9][a-z0-9_-]{0,62}$/ — lowercase letters, digits, " + "hyphens, underscores. Got: '%s'."
|
|
11148
|
+
}), 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}$/, {
|
|
11149
|
+
message: "notion_workspace.databases values must be Notion database " + "UUIDs (32 hex characters, optional dashes)."
|
|
11150
|
+
})).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."),
|
|
11151
|
+
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."),
|
|
11152
|
+
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.")
|
|
11153
|
+
}).optional();
|
|
11144
11154
|
var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
11145
11155
|
account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11146
11156
|
message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
|
|
@@ -11154,6 +11164,13 @@ var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
|
11154
11164
|
}).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)."),
|
|
11155
11165
|
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).")
|
|
11156
11166
|
}).optional();
|
|
11167
|
+
var AgentNotionWorkspaceConfigSchema = exports_external.object({
|
|
11168
|
+
databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
|
|
11169
|
+
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."
|
|
11170
|
+
})).min(1, {
|
|
11171
|
+
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."
|
|
11172
|
+
}).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.")
|
|
11173
|
+
}).optional();
|
|
11157
11174
|
var ReactionsSchema = exports_external.object({
|
|
11158
11175
|
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."),
|
|
11159
11176
|
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`."),
|
|
@@ -11290,6 +11307,7 @@ var AgentSchema = exports_external.object({
|
|
|
11290
11307
|
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."),
|
|
11291
11308
|
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)."),
|
|
11292
11309
|
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."),
|
|
11310
|
+
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."),
|
|
11293
11311
|
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({
|
|
11294
11312
|
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."),
|
|
11295
11313
|
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.")
|
|
@@ -11413,6 +11431,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11413
11431
|
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."),
|
|
11414
11432
|
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)."),
|
|
11415
11433
|
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."),
|
|
11434
|
+
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."),
|
|
11416
11435
|
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."),
|
|
11417
11436
|
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)."),
|
|
11418
11437
|
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)."),
|
|
@@ -11908,6 +11927,34 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
11908
11927
|
mergeAgentConfig.notifiedWorkerIsolationMove = false;
|
|
11909
11928
|
})(mergeAgentConfig ||= {});
|
|
11910
11929
|
|
|
11930
|
+
// src/config/notion-workspace-acl.ts
|
|
11931
|
+
function validateNotionWorkspaceConfig(config) {
|
|
11932
|
+
const issues = [];
|
|
11933
|
+
const dbMap = config.notion_workspace?.databases ?? {};
|
|
11934
|
+
const known = new Set(Object.keys(dbMap));
|
|
11935
|
+
for (const [agentName, agentRaw] of Object.entries(config.agents ?? {})) {
|
|
11936
|
+
if (!agentRaw)
|
|
11937
|
+
continue;
|
|
11938
|
+
const dbFilter = agentRaw.notion_workspace?.databases;
|
|
11939
|
+
if (dbFilter === undefined)
|
|
11940
|
+
continue;
|
|
11941
|
+
if (dbFilter.length === 0) {
|
|
11942
|
+
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.`);
|
|
11943
|
+
continue;
|
|
11944
|
+
}
|
|
11945
|
+
if (config.notion_workspace === undefined) {
|
|
11946
|
+
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.`);
|
|
11947
|
+
continue;
|
|
11948
|
+
}
|
|
11949
|
+
for (const name of dbFilter) {
|
|
11950
|
+
if (!known.has(name)) {
|
|
11951
|
+
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.`);
|
|
11952
|
+
}
|
|
11953
|
+
}
|
|
11954
|
+
}
|
|
11955
|
+
return issues;
|
|
11956
|
+
}
|
|
11957
|
+
|
|
11911
11958
|
// src/config/loader.ts
|
|
11912
11959
|
class ConfigError extends Error {
|
|
11913
11960
|
details;
|
|
@@ -12027,6 +12074,10 @@ function loadConfig(configPath) {
|
|
|
12027
12074
|
}
|
|
12028
12075
|
applyAgentOverlays(config);
|
|
12029
12076
|
validateAllCronTopicAliases(config, filePath);
|
|
12077
|
+
const notionIssues = validateNotionWorkspaceConfig(config);
|
|
12078
|
+
if (notionIssues.length > 0) {
|
|
12079
|
+
throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
|
|
12080
|
+
}
|
|
12030
12081
|
return config;
|
|
12031
12082
|
}
|
|
12032
12083
|
function validateAllCronTopicAliases(config, filePath) {
|
|
@@ -11141,6 +11141,16 @@ var MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
|
11141
11141
|
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."),
|
|
11142
11142
|
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.")
|
|
11143
11143
|
}).optional();
|
|
11144
|
+
var NotionWorkspaceConfigSchema = exports_external.object({
|
|
11145
|
+
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."),
|
|
11146
|
+
databases: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
|
|
11147
|
+
message: "notion_workspace.databases friendly names must match " + "/^[a-z0-9][a-z0-9_-]{0,62}$/ — lowercase letters, digits, " + "hyphens, underscores. Got: '%s'."
|
|
11148
|
+
}), 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}$/, {
|
|
11149
|
+
message: "notion_workspace.databases values must be Notion database " + "UUIDs (32 hex characters, optional dashes)."
|
|
11150
|
+
})).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."),
|
|
11151
|
+
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."),
|
|
11152
|
+
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.")
|
|
11153
|
+
}).optional();
|
|
11144
11154
|
var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
11145
11155
|
account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11146
11156
|
message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
|
|
@@ -11154,6 +11164,13 @@ var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
|
11154
11164
|
}).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)."),
|
|
11155
11165
|
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).")
|
|
11156
11166
|
}).optional();
|
|
11167
|
+
var AgentNotionWorkspaceConfigSchema = exports_external.object({
|
|
11168
|
+
databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
|
|
11169
|
+
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."
|
|
11170
|
+
})).min(1, {
|
|
11171
|
+
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."
|
|
11172
|
+
}).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.")
|
|
11173
|
+
}).optional();
|
|
11157
11174
|
var ReactionsSchema = exports_external.object({
|
|
11158
11175
|
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."),
|
|
11159
11176
|
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`."),
|
|
@@ -11290,6 +11307,7 @@ var AgentSchema = exports_external.object({
|
|
|
11290
11307
|
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."),
|
|
11291
11308
|
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)."),
|
|
11292
11309
|
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."),
|
|
11310
|
+
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."),
|
|
11293
11311
|
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({
|
|
11294
11312
|
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."),
|
|
11295
11313
|
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.")
|
|
@@ -11413,6 +11431,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11413
11431
|
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."),
|
|
11414
11432
|
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)."),
|
|
11415
11433
|
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."),
|
|
11434
|
+
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."),
|
|
11416
11435
|
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."),
|
|
11417
11436
|
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)."),
|
|
11418
11437
|
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)."),
|
|
@@ -11908,6 +11927,34 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
11908
11927
|
mergeAgentConfig.notifiedWorkerIsolationMove = false;
|
|
11909
11928
|
})(mergeAgentConfig ||= {});
|
|
11910
11929
|
|
|
11930
|
+
// src/config/notion-workspace-acl.ts
|
|
11931
|
+
function validateNotionWorkspaceConfig(config) {
|
|
11932
|
+
const issues = [];
|
|
11933
|
+
const dbMap = config.notion_workspace?.databases ?? {};
|
|
11934
|
+
const known = new Set(Object.keys(dbMap));
|
|
11935
|
+
for (const [agentName, agentRaw] of Object.entries(config.agents ?? {})) {
|
|
11936
|
+
if (!agentRaw)
|
|
11937
|
+
continue;
|
|
11938
|
+
const dbFilter = agentRaw.notion_workspace?.databases;
|
|
11939
|
+
if (dbFilter === undefined)
|
|
11940
|
+
continue;
|
|
11941
|
+
if (dbFilter.length === 0) {
|
|
11942
|
+
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.`);
|
|
11943
|
+
continue;
|
|
11944
|
+
}
|
|
11945
|
+
if (config.notion_workspace === undefined) {
|
|
11946
|
+
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.`);
|
|
11947
|
+
continue;
|
|
11948
|
+
}
|
|
11949
|
+
for (const name of dbFilter) {
|
|
11950
|
+
if (!known.has(name)) {
|
|
11951
|
+
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.`);
|
|
11952
|
+
}
|
|
11953
|
+
}
|
|
11954
|
+
}
|
|
11955
|
+
return issues;
|
|
11956
|
+
}
|
|
11957
|
+
|
|
11911
11958
|
// src/config/loader.ts
|
|
11912
11959
|
class ConfigError extends Error {
|
|
11913
11960
|
details;
|
|
@@ -12027,6 +12074,10 @@ function loadConfig(configPath) {
|
|
|
12027
12074
|
}
|
|
12028
12075
|
applyAgentOverlays(config);
|
|
12029
12076
|
validateAllCronTopicAliases(config, filePath);
|
|
12077
|
+
const notionIssues = validateNotionWorkspaceConfig(config);
|
|
12078
|
+
if (notionIssues.length > 0) {
|
|
12079
|
+
throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
|
|
12080
|
+
}
|
|
12030
12081
|
return config;
|
|
12031
12082
|
}
|
|
12032
12083
|
function validateAllCronTopicAliases(config, filePath) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/cli/ack-first-pretool.ts
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
var REPLY_TOOL_NAMES = new Set([
|
|
5
|
+
"mcp__switchroom-telegram__reply",
|
|
6
|
+
"mcp__switchroom-telegram__stream_reply"
|
|
7
|
+
]);
|
|
8
|
+
var ACK_SENT_MARKER = "ack-sent.flag";
|
|
9
|
+
function decide(input, stateDir) {
|
|
10
|
+
const toolName = input.tool_name ?? "";
|
|
11
|
+
if (toolName === "") {
|
|
12
|
+
return { decision: "allow" };
|
|
13
|
+
}
|
|
14
|
+
if (REPLY_TOOL_NAMES.has(toolName)) {
|
|
15
|
+
return { decision: "allow" };
|
|
16
|
+
}
|
|
17
|
+
if (!stateDir) {
|
|
18
|
+
return { decision: "allow" };
|
|
19
|
+
}
|
|
20
|
+
const markerPath = join(stateDir, ACK_SENT_MARKER);
|
|
21
|
+
let ackAlreadySent = false;
|
|
22
|
+
try {
|
|
23
|
+
ackAlreadySent = existsSync(markerPath);
|
|
24
|
+
} catch {
|
|
25
|
+
return { decision: "allow" };
|
|
26
|
+
}
|
|
27
|
+
if (ackAlreadySent) {
|
|
28
|
+
return { decision: "allow" };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
decision: "block",
|
|
32
|
+
reason: "First call `mcp__switchroom-telegram__reply` with a brief one-line " + "acknowledgement in your persona's voice before using any other tool \u2014 " + 'e.g. "on it \u2014 checking", "good question \u2014 one sec", "let me dig in". ' + "This is the framework's enforcement of the human-feel design " + "(`reference/conversational-pacing.md` beat 1): a colleague answers " + "in a beat. The reply can be the entire short answer if you have one " + "ready (then no further work needed); otherwise it's a brief status " + "and you continue with the work after."
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function runHook() {
|
|
36
|
+
if (process.env.SWITCHROOM_DISABLE_ACK_FIRST_GATE === "1") {
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
const stdin = await readStdin();
|
|
40
|
+
let input;
|
|
41
|
+
try {
|
|
42
|
+
input = JSON.parse(stdin);
|
|
43
|
+
} catch {
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
const stateDir = process.env.TELEGRAM_STATE_DIR;
|
|
47
|
+
const decision = decide(input, stateDir);
|
|
48
|
+
if (decision.decision === "block") {
|
|
49
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason: decision.reason }) + `
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
async function readStdin() {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
58
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
59
|
+
process.stdin.on("error", reject);
|
|
60
|
+
if (process.stdin.isTTY)
|
|
61
|
+
resolve("");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (typeof process !== "undefined" && process.argv?.[1]?.endsWith("ack-first-pretool.mjs")) {
|
|
65
|
+
runHook().catch((err) => {
|
|
66
|
+
process.stderr.write(`ack-first-pretool: hook failed unexpectedly: ${err}
|
|
67
|
+
`);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
runHook,
|
|
73
|
+
decide,
|
|
74
|
+
ACK_SENT_MARKER
|
|
75
|
+
};
|