1445043649
634e5119e1
feat(auth): login + change_password re-issue JWTs with wid + role
...
After PR4 every protected request demands wid. Three JWT-issue paths
must encode it; T4.8/T4.9 covered /initialize and /register, and this
commit closes the remaining two:
- login_local calls _ensure_default_workspace (idempotent — returns
the existing default_workspace_id when set) so users who pre-date
PR4 are backfilled at login time, and signs the new cookie with
wid + role='owner'.
- change_password does the same, ensuring a password change does not
strip the workspace claim and lock the user out on their next
request.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:35:06 +08:00
1445043649
84701730da
feat(auth): /register auto-creates default workspace + owner membership
...
Mirrors T4.8: the regular registration path now invokes the same
_ensure_default_workspace helper so every new user lands with a
1-person workspace (owner) and a JWT carrying wid + role='owner'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:32:39 +08:00
1445043649
a657d17995
feat(auth): /initialize auto-creates default workspace + owner membership
...
initialize_admin now seeds a 1-person workspace immediately after the
admin user is created: WorkspaceRepository.create(name, slug, owner_id)
+ WorkspaceMembershipRepository.add(role='owner') + writes the new
workspace id back to users.default_workspace_id. The session JWT is
re-issued with wid + role='owner' so subsequent requests pass the
T4.7 workspace gate.
Mechanical pieces:
- SQLiteUserRepository row<->user mapping now includes
default_workspace_id (sql update_user too) so the column persists.
- workspace.sql.SLUG_BLACKLIST is now public (was _SLUG_BLACKLIST) and
the registration helper treats blacklisted slugs as "taken" so the
walker steps past reserved names like "admin" instead of crashing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:31:53 +08:00
1445043649
a06e88d58b
feat(auth): workspace slug helpers — auto_slug_from_email + next_available_slug
...
auto_slug_from_email maps an email's local-part to a schema-valid base
slug (lowercase, [+_.]→'-', alphanumeric-and-hyphen, clamp 32 chars,
fallback to user-{token_hex(4)} for pathological inputs).
next_available_slug walks the {base, base-2, base-3, ...} sequence
against a caller-supplied async exists_check until it lands on a free
slot, truncating base when the suffix would push past 32 chars.
Pulled forward of T4.8/T4.9 because both /auth/initialize and
/auth/register need it. Lives in app.gateway.auth (not persistence)
since "email → slug" is a registration-time concern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:27:42 +08:00
1445043649
2145d36744
feat(auth): AuthMiddleware injects workspace ContextVar from JWT wid/role
...
deps.get_current_user_from_request now stashes the decoded payload on
request.state.auth_payload so AuthMiddleware can populate the
workspace_context ContextVar without a second decode. Reset is paired
in the same try/finally as user_context to keep teardown atomic.
Also adds:
- auth.models.ActiveWorkspace — minimal proxy that satisfies the
CurrentWorkspace protocol (id + role only).
- auth.models.User.default_workspace_id — surfaces the DB column added
in T4.4 so the eventual /auth/me payload can reference it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:25:43 +08:00
1445043649
b4bef65079
feat(auth): decode_token rejects legacy 4-field JWTs as WORKSPACE_MISSING
...
After PR4 every JWT must carry wid (workspace_id). decode_token now
returns the new TokenError.WORKSPACE_MISSING when the signature is
valid but the payload lacks wid; expired tokens still report EXPIRED
first so /auth/refresh logic stays correct. AuthErrorCode gains a
matching WORKSPACE_REQUIRED for middleware to surface to clients.
Updates 13 existing test sites that issued tokens without wid to pass
workspace_id="ws-test" + role="owner", reflecting the new contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:22:10 +08:00
1445043649
54cb94c30f
feat(auth): JWT TokenPayload accepts wid + role
...
TokenPayload gains optional wid (workspace_id) and role claims;
create_access_token accepts them as keyword-only args and only
encodes them when provided. Existing tokens and existing callers
keep working unchanged — the contract that protected requests
must carry wid is enforced by middleware (PR4 T4.6/T4.7), not by
the JWT type system.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:17:55 +08:00
1445043649
54762f491c
feat(persistence): UserRow.default_workspace_id
...
Adds a nullable FK column to mirror alembic 0001. On fresh deployments
metadata.create_all() will create users with this column; existing
deployments rely on the alembic migration to add it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:15:05 +08:00
1445043649
917d8fbeaf
test(persistence): alembic 0001 round-trip on SQLite + Postgres
...
Verifies alembic upgrade 0001 adds users.default_workspace_id (with FK
to workspaces) and that downgrade cleanly removes it. Runs on both
dialects because the migration uses op.batch_alter_table for SQLite
ALTER compatibility.
Tests are intentionally sync — alembic's command layer is sync and
env.py calls asyncio.run(); running under pytest-anyio would deadlock
on the inner event loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:11:11 +08:00
1445043649
8efbb2f9e5
feat(persistence): alembic 0001 — users.default_workspace_id
...
First Alembic revision for the DeerFlow application schema. Adds a
nullable users.default_workspace_id column with an ON DELETE SET NULL
FK to workspaces(id), so newly-registered users can be routed back to
their default workspace on next login without consulting memberships.
Uses op.batch_alter_table for SQLite ALTER compatibility (sqlite is
still a supported dev fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:08:44 +08:00
1445043649
d98498b705
docs(impl): T4.1 — confirm alembic baseline is not needed
...
alembic heads/history both empty, versions/ dir empty: 0001 can be the
first revision with down_revision=None. alembic_version table will be
auto-created on first upgrade head. doctor.py needs no new check.
Also records T4 preparation grep findings on _ensure_admin_user current
behavior (open issue #3 resolved).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:07:34 +08:00
1445043649
c70c6594de
docs(impl): STATUS — record LOCK sign-off and origin push
...
Two unblocking events on 2026-05-12: team signed off all 7 irreversible
schema decisions, and docs branch was finally pushed to origin (38 commits,
via SSH-over-443 to bypass local proxy). PR4 is now unblocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 22:02:33 +08:00
1445043649
a592319e3c
docs(impl): STATUS update after PR3 merge
...
PR3 ✅ merged 进 docs branch(7 commits, T3.1-T3.10)+ RDS live 验证通过
(11 张表含 workspaces/workspace_memberships,partial unique on owner 索引
建出来)。测试基线 3134 passed + 25 skipped + 0 failed。
下一个 PR4(auth 改造)plan 推荐 Inline 模式(跨 jwt.py/auth_middleware.py/
routers/auth.py 多文件耦合紧)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:34:05 +08:00
1445043649
dda8264057
docs(impl): PR3 implementation note + acceptance checklist
...
记录 Stage 0 PR3 的实施落点、LOCK 决策、跟进项与 6 commit 列表。
验收:3134 passed + 25 skipped + 0 failed(PR2 末 3087 + 47 新 ws_context/
ws_repo/membership_repo 测试 + 2 PG-only skipped)。RDS live 验证通过:
workspaces / workspace_memberships 表 + partial unique on owner 都建出来。
T3.6 (membership ORM) 提前到 T3.4-T3.5 (workspace repo) 之前,因为
WorkspaceRepository.get/list_by_user 会 JOIN memberships。
Stage 0 PR3 T3.10. PR3 完成。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:18:19 +08:00
1445043649
3313047ff1
test(workspace): partial unique on owner — Postgres twin test
...
新建 test_workspace_partial_unique_postgres.py,2 个 @pytest.mark.postgres
test,用 PR1 的 testcontainers postgres_url fixture 验:
- test_partial_unique_on_owner_enforced_on_postgres:第 2 个 owner
IntegrityError(SQLite twin 在 test_workspace_membership_repo 已覆盖)
- test_multiple_admins_allowed_on_postgres:多个 admin/member 不触发约束
为什么 PG 单独写:sqlite_where vs postgresql_where 是两份 DDL;SQLAlchemy
能 emit 不代表 PG 真的执行。本测试 pin 两边语义一致。
本地无 docker daemon → SKIPPED;CI workflow backend-postgres-tests 会实跑。
Stage 0 PR3 T3.8。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:15:25 +08:00
1445043649
36ffe2713f
feat(persistence): WorkspaceMembershipRepository + 8 unit tests
...
新建 backend/packages/harness/deerflow/persistence/workspace_membership/sql.py,方法:
- add(workspace_id, user_id, role, invited_by=None) 校验 role ∈
{owner, admin, member}(Stage 0 仅写 owner,schema 已支持其余)
- remove(workspace_id, user_id) → bool(rowcount > 0)
- list_by_user(user_id) 按 joined_at desc
- list_by_workspace(workspace_id) 按 joined_at asc
- get_role(workspace_id, user_id) → str | None
- change_role(workspace_id, user_id, new_role) → bool
MembershipValidationError 自定义异常(role 非法)。
8 test 覆盖:
- add → get_role round-trip
- remove 命中/未命中返回 True/False
- 同 workspace 第 2 个 owner 触发 IntegrityError(partial unique)
- 同 workspace 多个 admin 不触发(Stage 2 forward compat)
- CASCADE: 删 user 自动清理 memberships
- list_by_user 按 joined_at desc 排序(多 workspace)
- role 校验拒绝 'viewer'
- change_role 命中改值 + 未命中返 False
注:owner 转让需要事务内两行原子 swap(先把现 owner 改 admin,再把新 owner
插 owner),放 PR4+ auth router 内做带权限检查的版本;本仓储不暴露
transfer_ownership 方法以保持单一职责。
Stage 0 PR3 T3.7。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:13:52 +08:00
1445043649
a40df03521
feat(persistence): WorkspaceRepository + 23 unit tests
...
新建 backend/packages/harness/deerflow/persistence/workspace/sql.py,方法:
- create(name, slug, owner_id, *, workspace_id=None, status='active')
UUID v4 自动生成;强校验 slug 格式(regex ^[a-z0-9](-?[a-z0-9])*\$ + 3-32
长度)+ slug 黑名单(25 个保留字)+ status 枚举
- get(workspace_id, *, user_id=AUTO) JOIN workspace_memberships 做成员校验;
user_id=None 显式 bypass(迁移/admin)
- get_by_slug(slug) 不带成员校验(path-based routing 用:先 slug→workspace_id
再到 route handler 里查成员)
- list_by_user(*, user_id=AUTO) 列 user 所属所有 workspace
- update_status / delete platform-admin 操作,不带成员校验
WorkspaceValidationError 自定义异常(slug 格式 / 黑名单 / status)。
23 test 覆盖:
- CRUD smoke + get_by_slug missing
- 重复 slug → IntegrityError
- 8 个 invalid slug pattern(短/长/大写/空格/破折号位置/连续破折号/下划线)
- 6 个 blacklisted slug
- status 状态机 + 非法值拒绝
- delete CASCADE 到 memberships(SQLite FK PRAGMA 已开启)
- get/list 成员过滤(user-A 看不见 user-B 的 workspace)
- list user_id=None 显式 bypass
全部在 SQLite ephemeral DB 上跑(< 1s)。partial-unique 双驱动验证留给 T3.8。
Stage 0 PR3 T3.4 + T3.5。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:11:28 +08:00
1445043649
d2d2d29c34
feat(persistence): add WorkspaceMembershipRow ORM model
...
新建 backend/packages/harness/deerflow/persistence/workspace_membership/
{__init__.py, model.py},注册到 Base.metadata。
WorkspaceMembershipRow schema(来自 workspace-schema-design §2.2 锁定版):
- workspace_id String(36) FK workspaces.id ON DELETE CASCADE — PK part
- user_id String(36) FK users.id ON DELETE CASCADE — PK part
- role String(16) NOT NULL(Stage 0 仅写 'owner',schema 允许 'admin'/'member'
为 Stage 2 RBAC 准备)
- invited_by String(36) FK users.id ON DELETE SET NULL nullable(Stage 2
invitation 流程用)
- joined_at DateTime(tz=True)
索引:
- idx_workspace_memberships_user (user_id, workspace_id) — 倒查索引让
/auth/me 列 user 所属 workspaces 走索引
- idx_one_owner_per_workspace partial UNIQUE on workspace_id WHERE role='owner'
—— 一 workspace 严格 1 个 owner;sqlite_where + postgresql_where 双驱动
并存(实测两边都识别)
Stage 0 PR3 T3.6(提前到 T3.4-T3.5 之前;WorkspaceRepository.list_by_user
等会 JOIN 这张表)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:09:19 +08:00
1445043649
8323bf68d2
feat(persistence): add WorkspaceRow ORM model
...
新建 backend/packages/harness/deerflow/persistence/workspace/{__init__.py, model.py}
+ 在 persistence.models.__init__ 注册 WorkspaceRow 让 Base.metadata.create_all
能自动建表。
WorkspaceRow schema(来自 workspace-schema-design §2.1 锁定版):
- id String(36) PK(UUID v4 字符串,与 users.id 对齐)
- name String(64) NOT NULL(显示名)
- slug String(32) NOT NULL UNIQUE(URL 标识,正则 ^[a-z0-9](-?[a-z0-9])*$)
- status String(16) default 'active'(active/suspended/deleted)
- owner_id String(36) FK users.id ON DELETE RESTRICT(删 owner 时阻拦,
必须先转让所有权)
- created_at / updated_at DateTime(tz=True)
每列 + 表都带中文 comment(沿用 commit 9ff79055 的 convention)。
Stage 0 PR3 T3.3。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:07:30 +08:00
1445043649
f63089aea8
feat(runtime): add workspace_context module + tests
...
新建 backend/packages/harness/deerflow/runtime/workspace_context.py,仿
user_context.py 的 API 形态:
- CurrentWorkspace Protocol(要求 .id: str + .role: str)
- _current_workspace ContextVar + set/reset/get/require
- DEFAULT_WORKSPACE_ID = "default" + get_effective_workspace_id(fallback
友好,不抛错;用于文件系统路径)
- AUTO 哨兵 + resolve_workspace_id 三态(AUTO/str/None)
与 user_context 的区别:CurrentWorkspace 额外要求 .role 字段,让 Protocol 同时
约束"workspace 是哪个"和"caller 在该 workspace 内的角色"(Stage 0 只见 'owner',
Stage 2 RBAC 打开 admin/member)。
backend/tests/test_workspace_context.py 16 个 test,覆盖:
- 4 个 set/reset/require 行为
- 3 个 Protocol structural check(接受 .id+.role / 拒少 .role / 拒少 .id)
- 4 个 get_effective_workspace_id(含 UUID → str 强转)
- 5 个 resolve_workspace_id 三态(AUTO/AUTO 无 ctx 抛错/explicit str/explicit
None/AUTO 强转 str)
Stage 0 PR3 T3.1 + T3.2。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-12 21:04:49 +08:00
1445043649
cbbb83a706
docs(impl): STATUS update after PR2 live verification on RDS
...
PR1 + PR2 已 live 验证:远程 Aliyun RDS (PostgreSQL 17.9) 上 9 张表
(DeerFlow 5 + LangGraph 4) 全部 create_all 成功。
记 3 个 PR2 follow-up commits(不在原 plan task list 但 live 必需):
- testcontainers image 16→17 对齐 RDS 大版本
- serve.sh auto-add --extra postgres 防 uv sync 卸 asyncpg
- async_provider 剥 +asyncpg dialect 前缀让 LangGraph saver 能解析 URL
更新"一句话状态" + 标 PG live 验证(区分 testcontainers 路径仍未实跑)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 18:01:22 +08:00
1445043649
39f8e117e8
fix(checkpointer): strip SQLAlchemy dialect prefix before passing to LangGraph
...
DeerFlow 用同一个 \`postgres_url\` 喂两条路径:
- SQLAlchemy 引擎(需 \`postgresql+asyncpg://\`)
- LangGraph AsyncPostgresSaver(psycopg 直连,需 libpq 风格 \`postgresql://\`)
PR2 把 .env / config.example.yaml 默认 URL 改成 \`+asyncpg\` 形态后,LangGraph
saver from_conn_string 会被 psycopg parser 直接抛 ProgrammingError
'missing "=" after "postgresql+asyncpg://..."'。
修:在 async_provider.py 传给 AsyncPostgresSaver 之前用正则剥掉
\`postgresql+\\w+://\` → \`postgresql://\`。一行修复,让同一个 URL 同时满足
SQLAlchemy 和 LangGraph 两条路径。
实测:make dev-daemon 起 gateway 成功,远程 RDS 上 9 张表(DeerFlow 5 +
LangGraph 4)全部 create_all 出来。
Stage 0 PR2 follow-up(不在原 plan task list 内,是 live make dev 触发的)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 17:54:57 +08:00
1445043649
bdef6c6b9f
fix(serve): auto-install postgres extras when config.yaml selects postgres
...
scripts/serve.sh 在 \`uv sync --quiet\` 前 grep config.yaml 探测
\`database.backend: postgres\`;若是则追加 \`--extra postgres\`,否则保持原状。
修一个 PR1+PR2 没覆盖到的隐患:serve.sh 每次 dev 启动都跑 uv sync,会把
之前手动 \`uv sync --extra postgres\` 装的 asyncpg 卸掉,导致 gateway
启动时 ImportError。
Stage 0 PR2 follow-up(不在原 plan task list 内,是 live 验证时发现)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 17:35:23 +08:00
1445043649
7f17cb8fee
chore(test): bump testcontainers Postgres to 17-alpine
...
\`make doctor\` 实测远程 Aliyun RDS = PostgreSQL 17.9。把
backend/tests/fixtures/postgres.py 的 testcontainers 镜像从
postgres:16-alpine 调到 postgres:17-alpine,与生产对齐。
同步把 STATUS.md "RDS 大版本对齐" 项标 done。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 17:29:16 +08:00
1445043649
ad22242ecc
docs(impl): add Stage 0 progress dashboard (STATUS.md)
...
新增 docs/multi-tenant-redesign/03-impl/STATUS.md 作为 Stage 0 唯一的"现在
到哪了"权威来源:
- 8 PR 状态表(PR0-PR2 ✅ merged,PR3-PR8 🟡 pending)
- 用户必须跟进的事(live PG 实跑 / RDS 大版本对齐 / push origin / LOCK
review / 远程 RDS 密码轮换)
- 跳过/推迟的子任务(T1.10 docker-pending、T2.7 acked、T2.8 optional
skipped、T2.9 backend/CLAUDE.md 待补)
- 即将遇到的开放问题(PR4 alembic baseline / _ensure_admin_user 现状 /
PG 大版本)
- PR3 模式选择 trade-off(Inline vs Subagent-Driven)
- 维护规则:完成 PR 后必更新;新 session 首先读本文件
填补 plan/impl notes 之外的空白:plan 是静态的,impl notes 是 per-PR
快照;STATUS 是跨 PR 的执行状态视图。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 16:44:09 +08:00
1445043649
1112a1971b
docs(impl): PR2 implementation note + acceptance checklist
...
记录 Stage 0 PR2 的实施落点、关键 LOCK 决策、跟进项与 7 commit 列表。
验收:3087 passed + 23 skipped + 0 failed(vs PR1 基线 3085 + 2 新
default-backend 测试)。
Stage 0 PR2 T2.10. PR2 完成。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 09:23:16 +08:00
1445043649
c53295dfee
docs(readme): add Database backend section in Quick Start
...
Quick Start 加 Step 3 "Database backend (Stage 0+ defaults to Postgres)":
- 说明 config.example.yaml 默认 postgres + DATABASE_URL 写 .env
- 给本地 dev 起 docker compose postgres 一行命令
- 提示 make doctor / make dev preflight 行为
- <details> 折叠 SQLite fallback 说明(offline dev 用)
Stage 0 PR2 T2.9(README 部分;backend/CLAUDE.md follow-up)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 09:20:26 +08:00
1445043649
745a33e05d
chore: T2.7 acknowledgement (covered by T1.8)
...
Plan T2.7 = "setup_wizard.py 推荐 postgres,引导填 DATABASE_URL"。
T1.8 commit eae01901 已经实现:
- 加 "Use Postgres? (y = postgres, n = sqlite)" 问答,默认 y
- 选 y 时引导填 DATABASE_URL,可留空稍后填 .env
- writer.build_minimal_config(database_backend='postgres') 输出
database.backend=postgres + postgres_url=\$DATABASE_URL
无新代码改动;本 commit 仅为 task tracking 完整性。
Stage 0 PR2 T2.7(acknowledged-only)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 09:17:51 +08:00
1445043649
83b680b2ea
feat(check): postgres preflight in scripts/check.py
...
新增 check_postgres_preflight() —— make dev 链 (check.py → serve.sh) 的最后一道
preflight:
- config.yaml 不存在 → silent skip(让 setup_wizard 引导)
- backend != postgres → silent skip
- DATABASE_URL 未设 → FAIL with hint
- postgres 设了但 host:port 3s socket 不通 → FAIL with 启 docker 提示
- 通则 OK + 显示 host:port
不在 serve.sh 里加:Makefile 已经把 check.py 串在 serve.sh 之前,FAIL 会
自然阻断启动;避免 bash + python 两处实现 PG 探测。
doctor.py 的 check_database 是事后诊断(make doctor);本 check 是事前
preflight(make dev/start)—— 互补。
Stage 0 PR2 T2.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 09:17:19 +08:00
1445043649
3e62a0f6ef
feat(docker): gateway depends_on postgres healthcheck (dev only)
...
dev compose gateway 段加 depends_on: { postgres: { condition: service_healthy } },
确保 postgres 通过 pg_isready 后 gateway 才启 uvicorn。
prod compose 不动:production 用远程 RDS(不在 compose 内),无 postgres
service 可 depend;docker-compose.yaml 不加此 depends_on。
YAML 通过 docker compose config 校验。
Stage 0 PR2 T2.5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 09:13:44 +08:00
1445043649
d312bdf968
test(config): pin default postgres backend + sqlite regression
...
新建 backend/tests/test_default_database_backend.py 两个测试:
- test_explicit_sqlite_backend_still_works (T2.3)
显式 database.backend=sqlite 仍生效;防 PR2 改默认后 SQLite 用户
悄无声息 regression
- test_config_example_default_backend_is_postgres (T2.4)
直接读 on-disk config.example.yaml,断言 database.backend == 'postgres'
且 postgres_url == '\$DATABASE_URL'(不允许硬编码凭据)
Stage 0 PR2 T2.3 + T2.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 09:12:28 +08:00
1445043649
7d3d3560ad
feat(env): activate DATABASE_URL with local-dev default
...
.env.example DATABASE_URL 行从注释改为活值(指向 docker compose 起的本地
postgres)。同时加 RDS 示例注释作为参考。psycopg2 风格 URL 升级到
postgresql+asyncpg:// 与 SQLAlchemy 异步 dialect 对齐。
Stage 0 PR2 T2.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 04:24:32 +08:00
1445043649
404135a16a
feat(config): default database backend to postgres
...
config.example.yaml database 段:postgres 成为活跃默认(postgres_url=\$DATABASE_URL),
SQLite 改为注释掉的 fallback 块(offline dev 用)。bump config_version 9→10。
为什么默认改 postgres:Stage 0+ 已要求 ALTER 4 张表加 workspace_id;
SQLite 加列后再迁 PG 是返工。Stage 0 没有生产数据,迁移阻力最小。
Stage 0 PR2 T2.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 23:03:52 +08:00
1445043649
85a14f4c05
docs(impl): PR1 implementation note + acceptance checklist
...
记录 Stage 0 PR1 的实施落点、关键 LOCK 决策、fixture 用法示例、跟进项与
10 commit 列表。验收:3085 passed + 23 skipped + 0 failed(无 regression),
PG smoke 在 docker daemon 起来后实跑(CI workflow 已配)。
Stage 0 PR1 T1.10. PR1 完成。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:58:21 +08:00
1445043649
a33b46b4af
ci(postgres): add Postgres workflow running @pytest.mark.postgres tests
...
新增 .github/workflows/backend-postgres-tests.yml,独立于现有
backend-unit-tests workflow:
- GitHub runner (ubuntu-latest) 自带 docker daemon
- testcontainers 在 runner 上起 postgres:16-alpine 容器
- uv sync --extra postgres-test 拉 testcontainers + asyncpg + psycopg
- pytest -m postgres 仅跑 PG 标签的 4 个 smoke test (+ 后续 PR 增量)
为什么独立 workflow 而不是合到 backend-unit-tests:
- 现有 fast 测试不受 PG container 启动延迟影响
- 可独立 fail-soft(早期 Stage 0 rollout 期间需要时加 continue-on-error)
- fork 用户不强制承担 docker infra 成本
Stage 0 PR1 T1.9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:55:53 +08:00
1445043649
eae0190184
feat(wizard): add Postgres backend question to setup wizard
...
setup_wizard.py 在 Step 3 (Execution) 之后新增一个 Database 问答:
"Use Postgres? (y = postgres, n = sqlite)",默认 y(Stage 0+ 推荐 PG)。
选 y 时引导填 DATABASE_URL(可留空稍后写 .env);DATABASE_URL 进 .env,
config.yaml 写入 database.backend=postgres + postgres_url=\$DATABASE_URL。
writer.py build_minimal_config 加 database_backend 参数,postgres 时
覆盖 base_config 的 database 段;默认 sqlite 时沿用 base_config 行为
(继承 config.example.yaml 的 sqlite_dir 等)。
minimal pattern:不新建 wizard/steps/database.py,inline 在 main 里加 1
问答 + writer 加 1 参数。后续如果需要更复杂数据库选项再升为完整 step 模块。
Stage 0 PR1 T1.8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:55:19 +08:00
1445043649
e6f5ba53bd
feat(doctor): add Database section probing configured backend
...
scripts/doctor.py 新增 check_database():
- sqlite → OK + 显示 sqlite_dir
- memory → WARN(数据非持久化)
- postgres → 解析 database.postgres_url 中的 \$DATABASE_URL,asyncpg
实际连接 + SELECT version() → OK with server version;连接失败时
FAIL 给出可执行 fix(启 docker compose postgres / 检查 DATABASE_URL)
- 未知 backend → WARN
Database section 插在 LLM Provider 与 Sandbox 之间。SQLite 部署不会触发
PG 探测(不破坏现有 dev 体验)。
Stage 0 PR1 T1.7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:52:26 +08:00
1445043649
8f480cd76f
test(persistence): postgres smoke tests for init_engine + ThreadMetaRepo
...
新建 backend/tests/test_postgres_smoke.py,4 个 test 用 postgres_url fixture:
- test_postgres_url_creates_isolated_database (T1.4)
asyncpg 直连验证 fixture 落到 test_<hex> DB
- test_postgres_url_isolates_between_tests (T1.4)
手动起第 2 个 DB 比对,证明每 test 隔离
- test_init_engine_postgres_creates_tables (T1.5)
init_engine('postgres', postgres_url) 跑完后 information_schema
出现 5 张现有表(users/threads_meta/runs/feedback/run_events)
- test_thread_meta_repo_postgres_round_trip (T1.6)
ThreadMetaRepository.create + get 完整 round-trip,验证 PG 上仓储行为
与 SQLite 对齐
全部 @pytest.mark.postgres 门控;Docker 不可用时由 fixture 跳过。本次本地
docker daemon 未起,4 测试 SKIP;CI workflow(T1.9)将带 docker service
触发实跑。
Regression:跑全套 `pytest tests/` 3085 passed + 23 skipped + 0 failed
(基线 3086 passed + 18 skipped;diff = 4 新 postgres skip + 1 env 相关
live test 偶发 skip)。
Stage 0 PR1 T1.4 + T1.5 + T1.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:50:10 +08:00
1445043649
31361e2dcf
test(fixtures): add testcontainers Postgres fixture for Stage 0
...
新建 backend/tests/fixtures/postgres.py,提供两层 fixture:
- postgres_container (session 级):用 PostgresContainer 起 postgres:16-alpine
- postgres_url (function 级):每 test 一个 ephemeral DB,teardown 时
pg_terminate_backend 清掉残连后 DROP DATABASE
为什么 per-DB 而不是 per-schema:asyncpg + SQLAlchemy 不通过 URL 传 search_path,
per-DB 一次 ~50ms 开销可接受,让 test 代码不感知 schema。
Docker 不可用时 fixture 自动 pytest.skip 而非 error,dev 环境无 Docker
仍能跑其余 3086 个测试。
注册 pytest mark `postgres`、把 tests/ 加 sys.path 让 pytest_plugins
按 `fixtures.postgres` 路径解析(不加 tests/__init__.py 避免干扰
现有 pytest 发现行为)。
Stage 0 PR1 T1.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:44:02 +08:00
1445043649
ae4ea46be2
feat(docker): add postgres service to dev compose
...
新增 postgres:16-alpine service + named volume + healthcheck,作为 Stage 0+
默认 DB backend。生产可通过 DATABASE_URL 指向远程 RDS 时跳过此 service。
端口 / 用户 / 密码 / 库名都走 env var 覆盖(POSTGRES_USER/PORT/...
默认 deerflow/5432/deerflow_dev)。
YAML 通过 `docker compose config` 校验;live healthcheck 待 docker daemon 起后由
T1.3 testcontainers 路径覆盖。
Stage 0 PR1 T1.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:40:12 +08:00
1445043649
fab85b14a6
build(deps): add postgres-test optional extra for testcontainers
...
新增 postgres-test extra(harness + backend),引入 testcontainers[postgres]
4.14.2 用于 Stage 0 PR1 的 PG fixture。复用现有 postgres extra 的
asyncpg/psycopg/langgraph-checkpoint-postgres,pytest-asyncio 沿用 dev group。
Stage 0 PR1 T1.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:38:50 +08:00
1445043649
a74b88a45c
docs(superpowers): add Stage 0 multi-tenant foundation master plan
...
8 PR 串成的 stage-level 拆解(Postgres 切换 + workspaces schema + auth
扩字段 + 入口路由强校验 + CI boundary + headless schema 预留)。每个 PR
含 commit-sized TDD task list 和验收清单。执行时各 PR 独立 review
checkpoint。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 22:33:25 +08:00
1445043649
89fe54cc07
docs(multi-tenant): 加入汇总索引、跨文档一致性修订、Postgres 切换前移到 Stage 0
...
* 新增 README.zh-CN.md 汇总索引:ADR 状态表 + Stage 0-4 业务目标 / 技术路径 /
验证方式 + Stage↔ADR 对照矩阵 + 不可逆决策一览 + 用语映射 + FAQ + 阅读路径
* 新增 workspace-schema-design.zh-CN.md(Stage 0 schema 锁定版)一并入库
* 7 份 ADR 顶部加"代码命名"映射行(tenant_id ↔ workspace_id)
* ADR-002 §1 加分期落地提示,明确 K8s 推迟到 Stage 3
* ADR-005 §5"第 1/2 阶段"补出与 rollout Stage 2/3 的映射
* ADR-007 §4 加 /api/v1/ 反向链接;§8 加 tid → wid 字段名映射
* headless-api §0/§7 把"SaaS + on-prem 双主线"改为"SaaS 主线、schema 兼容 on-prem"
* phased-rollout 去除重复的"Go/No-Go 进入 Stage 2"段
Postgres 切换从 Stage 1 提前到 Stage 0:Stage 0 已要 ALTER 4 张表加
workspace_id,先 SQLite 再 PG 是纯返工;Stage 0 没有生产数据,迁移阻力最小。
同步调整 phased-rollout / headless-api / phase-0-plan / workspace-schema-design /
README 中的时间盒(Stage 0: 3-4→4-5 周;Stage 1: 10-15→8-13 周)、不可逆决策
清单、PR 顺序、轨道前置依赖。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 21:39:58 +08:00
1445043649
9ff790554d
docs(persistence): 给 ORM 表/字段补充中文注释
...
通过 SQLAlchemy 的 comment= 给 5 张持久化表(users / threads_meta / runs /
run_events / feedback)的所有字段以及表本身加上中文注释,便于读代码、
生成文档与未来切到 Postgres 时直接落库为 COMMENT ON。
SQLite 引擎本身不支持 COMMENT ON,运行时不会改变 .schema 输出。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-10 18:50:17 +08:00
1445043649
ecc9339ede
docs(multi-tenant): 加入 Headless API 改造轨道(Pattern A/B)+ Stage 1 双轨修订
...
新增 headless-api-track 文档,把 DeerFlow 作为后端服务的两种集成
pattern 全部纳入设计:
- Pattern A(业务 backend 代理):业务系统 backend 用 API key 调,
浏览器走他们的 backend。覆盖 IM channels + server-to-server 集成。
- Pattern B(自研 web 浏览器直连):业务 backend 调
/api/v1/auth/exchange-token 换 5-15 min 短期 JWT → 浏览器拿 JWT 直连
含 SSE。核心 4 件事:exchange-token endpoint、ServiceTokenAuthBackend
(AuthMiddleware 第三条路径)、workspaces.allowed_origins + CORS
中间件、SSE 跨域验证。
- 不做 widget / iframe(纯 API)。
数据模型:service_accounts / api_keys / external_users 表(schema 在
Stage 0 末加上不阻塞);身份模式三态(collapsed / external_passthrough
/ both)按 endpoint 分支。
Stage 1 改为三轨并行:付费 SaaS / Pattern A / Pattern B;时间盒从
6-10 周延到 10-15 周。Pattern B 依赖 Pattern A 完成,建议 Stage 1
末 1-2 周做。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 23:29:46 +08:00
1445043649
fd8d0d637d
docs(multi-tenant): 加入 Stage 0 代码地图
...
把 Stage 0 必做项落到具体文件 + 行号,覆盖 7 个子系统:
auth、user 数据模型、thread 入口路由、ThreadDataMiddleware + 路径系统、
仓储访问模式、setup / 注册流程、CSRF + ContextVar 注入。
文档结构:每节有【关键文件 + 行号】、【关键函数 / 类】、【当前数据流】、
【Stage 0 改动锚点】;外加改动影响面总览(每条 Stage 0 必做项映射到
具体代码位置)、不可逆决策落点、推荐阅读顺序(11 个文件,1.5-2.5h
粗读)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 23:29:27 +08:00
1445043649
35ae97d0a6
docs(multi-tenant): Stage 2 加入 per-user skill 覆盖与 config 设计
...
补充团队 workspace 内不同成员的 skill 区分模型:
- 启用状态:workspace 级默认 + per-user 覆盖
(final_enabled = user_override ?? workspace_default)
- skill 私有配置(API key 等):per-user 强制隔离 + KMS 加密
- 上传权限:仅 owner / admin,成员只能 enable/disable + 填自己 config
PR 顺序新增第 8 步,依赖 RBAC + KMS 已就位。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 22:47:05 +08:00
1445043649
e78ed687a6
docs(multi-tenant): 加入按规模分期的落地方案(02-rollout)
...
基于已收敛的目标客户画像(个人用户为主 + 小团队 / 中心化 SaaS /
freemium)把 7 份 ADR 拆成 5 期落地:
- Stage 0:workspace 模型立起来 + auth 收紧(不可逆决策集中在此)
- Stage 1:Postgres + Quota 必落(freemium 不上 quota = 信用卡递给攻击者)
- Stage 2:DeerFlow 表 RLS、KMS、ObjectStorage S3、付费分层
- Stage 3:K8s sandbox + BYO key + audit DB 拆分
- Stage 4:SSO / custom domain / per-tenant DB(按 enterprise 客户合同驱动)
关键取舍:① Stage 0 末就把 workspace_id 列加到 SQLite,避免 Stage 1
切 Postgres 时再补;② Quota 比 ADR-003 原稿提前一档到 Stage 1;③ K8s
sandbox 推到 Stage 3,AioSandbox + 出网白名单 + 资源限额撑到几千用户。
时间盒:最小可付费 4 个月(Stage 0+1)/ 风险可控增长 8 个月(+Stage 2)
/ 全功能 14 个月(+Stage 3)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 22:28:55 +08:00
1445043649
8bdd308ea0
docs(multi-tenant): 据审计对齐 ADR-002/003/004/005 与 phase-0 计划
...
非 spike 驱动的对齐改动:
- ADR-002/004/005:头部加现状提示 + 链接审计报告(沙箱出网/资源缺位、
token_version 已存在 MembershipCache 全新建、ObjectStorage/7 表/KMS
全部从 0 起)
- ADR-003 LLM 计费:修正 TokenUsageMiddleware 当前只 log 不持久化的描述;
补充 create_chat_model sync→async 改造的连带影响说明
- phase-0 计划:新增 §3.5 底座先行(Postgres 测试夹具 / ObjectStorage
Protocol / KMS 抽象 3 件并行做),时间盒 2 → 3 周;ADR-006/007 摘要
对齐;DoD 加底座骨架检查项
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 21:03:32 +08:00
1445043649
27c4f14233
docs(multi-tenant): 加入 ADR 审计 + spike,并据其修订 ADR-001/006/007
...
新增两份评审产出物:
- adr-vs-code-audit:7 份 ADR 与现状代码的差异核对,标注每条假设是否成立
- adr-spike-langgraph-postgres:实测 langgraph-checkpoint-postgres==3.0.5
注入能力,确认不存在 connection_factory 参数,且 psycopg_pool 自带的
configure callback 不是 per-acquire hook
据 spike 与审计修订三份 ADR:
- ADR-001 数据隔离:LangGraph 表改为应用层强校验 + threads_meta unique
约束兜底(不再挂 RLS、不 ALTER 表);hook 点从 AssistantsCompat 修正
为 threads.py + thread_runs.py
- ADR-006 运行时与渠道:§2.1 完全重写为应用层强校验;MCP OAuth token
从"无持久化进程内存"直接做加密 DB;channel store binding 改为新建
channel_bindings 表
- ADR-007 路由与前端:删除 Better Auth 假设(前端实际无此依赖),改为
扩展现有 auth/jwt.py TokenPayload 加 tid/role 字段
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 21:03:16 +08:00