架构
多租户
多租户是代码库中最重要的不变量。 跨工作区边界泄漏数据的错误是安全事件。 强制执行是分层的:traits、全局范围、策略和使构建失败的回归测试。
Trait 模式
两个 traits 承担繁重的工作:
App\Concerns\BelongsToWorkspace— 对于具有直接workspace_id列的模型。App\Concerns\BelongsToAgent— 对于属于智能体的模型(并间接属于该智能体的工作区)。trait 的全局范围通过agents连接以按agents.workspace_id = current过滤。
两个 traits 都添加一个应用于模型的每个 Eloquent 查询的全局范围。
它们还在创建时自动填充 workspace_id,因此您不能意外地将行插入到错误的工作区。
CurrentWorkspace
当前工作区由中间件按请求解析:
- 认证请求 — 读取
users.default_workspace_id,验证成员资格,设置单例。 - 小部件请求 — 从 JWT 的
agent_id声明派生,查找智能体,从agents.workspace_id设置单例。 - 系统/队列作业 — 每个作业显式设置,从不继承。
解析器位于 App\Support\CurrentWorkspace。
请求后,中间件的 finally 块清除它,因此状态不能在 Octane 请求之间泄漏。
绕过范围(罕见)
某些查询确实需要跨工作区查看 — 管理员报告、模拟、系统作业。 绕过是显式的:
// System-level operation: rolling up usage across all workspaces
Workspace::withoutWorkspaceScope()->each(...)
约定是每个 withoutWorkspaceScope()
调用必须在其正上方有正当理由注释。
代码审查强制执行此操作;/tenancy 审计斜杠命令标记违规。
切换工作区
具有多个成员资格的用户使用侧边栏中的工作区切换器。
切换 POST 到 /workspaces/{id}/select,
更新 users.default_workspace_id 并重定向。
下一页加载解析新工作区。
切换器仅在用户有两个或多个成员资格时呈现 — 单工作区用户看到安静的标签而不是菜单,因为在一个选项中“切换”只是杂乱。
每工作区数据
租户范围的表(每个带有 trait 的 Eloquent 模型):
- 直接
workspace_id:agents、integration_connections、plan_subscriptions、usage_events、audit_logs、invitations。 - 通过智能体:
agent_versions、behavior_rules、cta_rules、curated_answers、experiments、sources、documents、chunks、visitors、conversations、messages、leads、content_gaps。
跨工作区表(无范围):
users— 全局。任何工作区的成员资格在枢轴中。workspaces— 全局。工作区本身不范围到工作区。plans— 全局,平台管理员管理。jobs/failed_jobs/notifications— Laravel 基础设施。app_settings— 单例,平台范围。
向量存储隔离
向量存储元数据镜像租户契约 — 每个点都有
agent_id 和 workspace_id 标签,
每个查询都按 agent_id 过滤。
Cloudflare Vectorize 和 Qdrant 都原生支持此功能(Qdrant 的有效负载过滤器 / Vectorize 的元数据过滤器)。
仅按 agent_id 过滤但不按智能体的实际工作区过滤的错误仍然是安全的 —
智能体 ID 是 ULID,全局唯一。
根本不过滤的错误将是泄漏。
Retriever 始终显式过滤。
授权(策略)
租户是“这行在我的工作区中吗?”。授权是“我可以用我的工作区中的行做什么?”。
策略位于 app/Policies/:
WorkspacePolicy— 查看/更新/删除工作区本身,转移所有权。AgentPolicy— viewAny / view / create / update / delete / publish / rollback。SourcePolicy— 管理智能体内的来源。LeadPolicy— 读取/更新/删除潜在客户。IntegrationConnectionPolicy— 连接/断开连接。
检查模式是一致的:$user->can('update', $agent)
或 abort_if(! $user->can(...))。
策略使用 Tenancy 帮助器解析用户在资源工作区中的角色,
然后调用 WorkspaceRole 枚举的能力方法。
测试
回归测试是 tests/Feature/ 下的 MultiTenancyTest。
它用重叠的数据种子两个工作区,并断言每个工作区的查询只能看到自己的行。
它还遍历 app/Models/ 中的每个模型,以确认任何具有
workspace_id 的模型都使用 trait。
运行:
php artisan test --filter=MultiTenancyTest
每次 CI 构建都必须通过。破坏它的 PR 不能合并。