-
Notifications
You must be signed in to change notification settings - Fork 2
bot_destination_scope
Status: shipped 2026-09-26 (phases 1–3 and Save & test).
A crew project has two path forms:
| Form | Example | Who uses it |
|---|---|---|
| Logical | Chats/Work/projects/<id> |
the crew UI; the user-scoped workspace API |
| Physical | _users/<owner>/Chats/Work/projects/<id> |
crew turns, the server's own file reads, owner lookup |
Workflows have one form (Workflow/<name>), so bot code written for workflows
stored and compared paths as plain strings. Crews were added on top, and each
place picked a form. On 2026-09-25/26 that caused, one per deploy:
- a crew's own Slack bot selected with the logical path: "product manifest not found";
- its Slack route with no owner: "This Slack route is no longer configured";
- shared-bot crew channels refused: "does not match the selected product workspace";
- legacy crew bots locking their owner out (403), and a repair dropping their channels.
The server reads files without a user identity, so a logical path names no
folder there, and routeWorkspaceUserID reads the owner only from a
_users/<id>/ prefix.
A stored crew destination is always physical. Every Slack store that holds
a crew path (the connection registry, a connection's channel routes, the
platform channel routes) holds _users/<owner>/.... The owner is part of the
path, and platform routes also carry workspace_user_id.
-
Edge: every bot endpoint converts an incoming crew path once, with
canonicalBotScope(a logical path names the caller's own crew; another owner's crew is always addressed physically). -
Storage: the Slack stores refuse a logical crew path
(
services.ValidateBotScope). A future caller that forgets the edge conversion fails loudly in tests instead of breaking in Slack. -
Migration: startup rewrites legacy logical crew paths to the one user
tree that holds the project (
migrateCrewBotScopes); zero or several candidates leave the record and log it.
Workflow paths are unaffected. WhatsApp keeps its per-user route store (its checks already compare for that user).
Every bot turn (workflow, crew conversation, default chat) gets its platform fields from one helper: platform, channel, thread and arrival connection. The crew-conversation builder once omitted the connection, so revalidation looked in the shared bot's routes and refused a crew's own bot.
A dry run feeds a synthetic mention through the real inbound path (route
resolution, turn building, revalidation, access) and stops before the model.
It backs the end-to-end tests (bot_dry_run_test.go: own crew bot, legacy
logical crew bot after migration, shared bot to a crew channel, workflow bot,
unrouted channel) and the Slack tab's Save & test. It covers Slack only;
WhatsApp keeps its own per-user routing and has no dry run yet.
dryRunSlackMention (bot_dry_run.go) runs the app's mentionMessage (shared
with the app-mention handler), a bot manager copied from the production one
(RunDryRun; the wiring lives in wireBotManager for startup and tests), and
the query boundary (revalidateExecutionPrincipal plus admitQueryTarget,
shared with handleQuery). It skips only bindSlackInvocation, which
allocates a workflow trigger's run folder. A refusal carries the reply the
user would get. Save & test calls it through
POST /api/human-feedback/slack/connections/{id}/dry-run after the token
checks pass (frontend/.../bots/slackDryRun.ts).
Before the invariant, several checks were taught to accept both forms
(SameSlackScopePath, slackRouteFolderMatches, workspacePathsMatchForUser
in route checks, the owner lookup in dedicatedSlackProfileRoute). With the
migration and the storage guard they are no longer load-bearing; they stay as
defense for records written by an older client during an upgrade. New code
relies on the invariant, not on them.
Auto-synced from docs/ on main. Edit there, not here.