Commit Graph

2195 Commits

Author SHA1 Message Date
1445043649 2d982c3f0b feat(authz): data-plane allowlist helper + INSUFFICIENT_SCOPE code (Stage 1 收口)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:25:46 +08:00
1445043649 1b7d8217dd docs(stage-1): implementation plan for control-plane default-deny
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:09:31 +08:00
1445043649 a173d4f93c docs(stage-1): API key control-plane default-deny design spec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:05:14 +08:00
1445043649 cdd9d6701f docs(stage-1): tick all plan checkboxes to match landed commits (Stage 1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:49:34 +08:00
1445043649 4cd2acc192 feat(apps): 新增 headless(API Key) 多租户冒烟测试,并登记进 apps/README
multi_tenant.py 的「server-to-server / 无人值守」版:每个租户先由人类 owner
(cookie 会话)建 service account 并 mint 一把 workspace-scoped key
(POST /api/v1/service-accounts → POST /api/v1/api-keys,plaintext 仅返回一次),
之后所有对话只用 Authorization: Bearer dfk_live_...(独立 Session,免 cookie/CSRF)。

除并发 / 多轮上下文 / 租户隔离(同 cookie 版)外,额外校验两条 headless 专属性质:
- scope 强制:缺 runs:create 的 key 发起 stream 返回 403
- 撤销即失效:DELETE /api/v1/api-keys/{id} 后该 key 立即 401

对 :8001 实跑(DF_TENANTS=4 DF_TURNS=10)全绿。同时更正 apps/README 中
「API Key 鉴权尚未接入」的过时说明。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:45:37 +08:00
1445043649 37c3417bfb docs(stage-1): record known scope/least-privilege limitation from final review (Stage 1 PR5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:20:36 +08:00
1445043649 938c2eb0be harden(gateway): uniform 404 on API key revoke to hide cross-tenant existence (Stage 1 PR4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:19:20 +08:00
1445043649 dee1eb2374 docs(stage-1): backfill plan link in spec; sync plan with review-driven changes (Stage 1 PR5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:12:07 +08:00
1445043649 190c1dc3c8 feat(frontend): migrate API calls to /api/v1 (Stage 1 PR5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:07:20 +08:00
1445043649 e6a12b9a7c test(gateway): strengthen dual-mount coverage to a v1-twin invariant (Stage 1 PR5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:01:29 +08:00
1445043649 2461a923be feat(gateway): dual-mount legacy routers on /api and /api/v1 (Stage 1 PR5)
Strip /api prefix from 13 legacy router APIRouter() declarations and dual-mount
each on prefix="/api" (backward compat) and prefix="/api/v1" (versioned surface)
in app.py. Auth, service-accounts, api-keys, assistants-compat remain single-mount.
Update 10 test files to pass prefix="/api" when directly including stripped routers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:49:05 +08:00
1445043649 3f0d5c8c96 feat(gateway): X-API-Deprecated header for legacy /api/* paths (Stage 1 PR5)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:37:36 +08:00
1445043649 9d6decd91b test(gateway): end-to-end headless API mint/use/isolation smoke (Stage 1 PR4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:34:56 +08:00
1445043649 c3c4e57416 harden(gateway): block API key mint on inactive SA; cover cross-workspace IDOR (Stage 1 PR4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:25:17 +08:00
1445043649 950964e0ef feat(gateway): api-keys management endpoints with one-time plaintext (Stage 1 PR4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:19:32 +08:00
1445043649 84e06ca396 harden(gateway): constrain service-account role/identity_mode at API boundary (Stage 1 PR4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:15:40 +08:00
1445043649 d16e294185 feat(gateway): service-accounts management endpoints (Stage 1 PR4)
Owner/admin self-service CRUD for service accounts: POST create,
GET list, PATCH status; workspace-scoped with 404 existence hiding
for cross-workspace targets. Gated by require_workspace_admin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:11:13 +08:00
1445043649 e92fe0d7fb feat(authz): require_workspace_admin dependency (Stage 1 PR4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 19:07:55 +08:00
1445043649 9eb6103a4d feat(csrf): skip CSRF for bearer-header requests (Stage 1 PR3)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:06:23 +08:00
1445043649 78359c3fd8 harden(auth): guard bearer auth errors as 503; widen AuthContext to ServicePrincipal (Stage 1 PR2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:04:19 +08:00
1445043649 3ec4fb8537 feat(auth): AuthMiddleware bearer dfk_ path (Stage 1 PR2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:58:14 +08:00
1445043649 ac8b37bd27 test(auth): cover expired-key rejection in APIKeyAuthBackend (Stage 1 PR2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:44:41 +08:00
1445043649 d9a86878f4 feat(auth): APIKeyAuthBackend resolves token to SA principal (Stage 1 PR2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:40:27 +08:00
1445043649 4f9116e3fe fix(auth): keep CurrentUser protocol id-only; is_service_account is opt-in (Stage 1 PR2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:37:06 +08:00
1445043649 ec4769a33f feat(auth): ServicePrincipal + is_service_account discriminator (Stage 1 PR2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:31:49 +08:00
1445043649 978b0cf24d test(persistence): cover ExternalUserRepository.get + document upsert semantics (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:29:44 +08:00
1445043649 5093d3d123 feat(persistence): ExternalUserRepository scaffold (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:26:34 +08:00
1445043649 ac2ab26a7e refactor(persistence): ApiKeyRepository hot path via indexed prefix + constant-time hash verify (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:24:06 +08:00
1445043649 ab40a4a17e feat(persistence): ApiKeyRepository with active-key hot path (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:17:07 +08:00
1445043649 04170205dd fix(persistence): validate status in ServiceAccountRepository.create (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:13:14 +08:00
1445043649 9166ab205d feat(persistence): ServiceAccountRepository (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:08:20 +08:00
1445043649 427709e0a8 test(auth): polish token utility tests + docstring (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:05:20 +08:00
1445043649 e5ff6e74f9 feat(auth): API key token generation/hashing utilities (Stage 1 PR1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:01:03 +08:00
1445043649 ef32a6de0f docs(stage-1): headless API Pattern A auth foundation implementation plan
5-PR TDD plan derived from the Stage 1 spec: token utils + 3-table repos
(deerflow layer), APIKeyAuthBackend + AuthMiddleware bearer path, CSRF
bearer skip, owner/admin mint endpoints with cross-workspace 404, and
/api/v1 dual-mount migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:57:24 +08:00
1445043649 240c6bd0e2 docs(mt): 统一 multi-tenant-redesign 命名约定 + README 补执行层索引
命名统一为 .zh-CN.md 后缀(与既有 17 个文件 + README 一致):
- 03-impl/{pr1-8,STATUS}.md → *.zh-CN.md
- Stage 1 spec 去日期前缀、加 .zh-CN,对齐 01-redesign 语义命名

README.zh-CN.md 修复 4 处不统一:
- 顶部加进度指引(现状只信 STATUS,本文是设计/路线导航)
- §0 文档总图补 03-impl 层 + Stage 1 spec + 命名约定注
- §1 表加 Stage 1 spec 行;新增 §1.1 执行记录层(STATUS + 8 impl note 索引)
- §7 阅读路径首次进项目/Stage 1 均加 STATUS + spec 入口

同步更新所有交叉链接(STATUS/pr/spec 自引用、database-schema-as-built、
根 README_zh.md、Stage 0 master plan);全树相对链接校验可达。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:40:42 +08:00
1445043649 8ea5507d27 docs(spec): 将 Stage 1 鉴权地基 spec 移入 multi-tenant-redesign/01-redesign
与 Stage 0 设计(ADR / schema-design)并排,统一策展主线;
相对链接从 ../../multi-tenant-redesign/ 改为 ../。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:30:25 +08:00
1445043649 45b019dae8 docs(spec): Stage 1 headless API Pattern A 鉴权地基设计
承接 Stage 0(PR1-8 全 merge)与 headless-api-track 轨道二。
锁定 5 个 PR:三表仓储+token 工具 / APIKeyAuthBackend 双路径 /
CSRF skip on bearer / 管理 endpoint mint 闭环 / api/v1 全量迁移。
含 5 项 brainstorm 决策(D1 SA→CurrentUser 映射 / D2 管理 endpoint /
D3 全量迁 v1 / D4 scope 白捡 / D5 dfk_ key 格式)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:19:26 +08:00
1445043649 3a16da6449 docs(stage-0): 记录 multi_tenant.py live smoke PASS,关闭退出门工程项
2026-06-27 用户运行 apps/examples/http-chat/multi_tenant.py 打到运行中
Gateway,确认 verdict=PASS。该 smoke 覆盖退出门「注册→workspace 自建→
创建 thread→跨 workspace 互调 404」,且更强:N 租户真并发 + 多轮链式上
下文 + 双向隔离(search 不泄漏 + 跨租户 GET 404)。

- STATUS.md: 新增「Live smoke 结果」段(只记代码层核实的不变量,未编造
  租户数/轮数;标注 JWT wid claim 未显式断言的缺口);PR4 T4.14 标 、
  PR6 T6.15 标部分 done(文件迁移仍 );更新一句话状态 + 分支改名
- plan 退出门:勾上 5 条工程项(PR 合入/测试/CI/smoke/LOCK),仅剩
  2 条时间门 + 1 条业务门未关

纯文档,无代码改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:59:18 +08:00
1445043649 30fa16ba7a feat(apps): 新增 http-chat 多租户并发示例并登记进 apps/README
multi_tenant.py:app.py 的并发/多租户版,用 /api/v1/auth/register
并发创建多个租户(各自独立 workspace + Session),同时跑 N 轮链式
对话,校验真并发、多轮上下文按 thread 各自保持、租户隔离
(search 仅见己有线程 + 跨租户 GET 404)。轮数/租户数可配
(DF_TURNS / DF_TENANTS),唯一邮箱可重复跑且不触发限流。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:42:24 +08:00
1445043649 c77ee0dc06 docs(readme): 中文 README 改以多租户改造为主线 + 忽略 IDE/agent 产物
- README_zh.md:全面重写,把 workspace 租户模型 / Postgres 默认后端 /
  行级隔离 / 扩展 JWT / per-workspace 路径 / Headless API schema / apps
  脚手架作为主线,新增「多租户改造」「数据库后端」「多租户架构详解」
  「apps/」章节,并补 Stage 0–4 路线图与文档入口
- .gitignore:忽略 .qoder/(Qoder repowiki 产物)与 AGENTS.md(agent 指令)
- verify_stage0.sh:e2e 校验改用 verify-stage0.com 邮箱域、先 initialize
  admin、切到 /api/v1/ auth 路径

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:15:48 +08:00
1445043649 45efba50e4 docs(db): 补充数据库设计落地版(as-built)并登记进 README
新增 docs/multi-tenant-redesign/01-redesign/database-schema-as-built.zh-CN.md:
对照实现代码(persistence/*/model.py + base.py/engine.py + alembic 0001-0003)
生成的事实参考,补齐之前只有「锁定版」(动手前决策)而缺失的落地 schema 文档。

涵盖:
- 持久化层总览:memory/sqlite/postgres 三后端、create_all vs Alembic、
  Postgres 库自愈、SQLite WAL、JSON ensure_ascii=False、partial index 双 where 兼容
- LangGraph checkpointer/store 表不归 ORM 管的说明
- ER 图(mermaid)+ 10 张表全字段参考(users/workspaces/workspace_memberships/
  threads_meta/runs/run_events/feedback/service_accounts/api_keys/external_users)
- 外键与删除策略矩阵(CASCADE/RESTRICT/SET NULL)
- 迁移历史 0001-0003(含 0002→0003 两段式上线:可空列→回填→锁 NOT NULL)
- 与锁定版的差异(run_events 已确认为 DB 表、runs token 分项列、status 状态机等)

README.zh-CN.md 的「文档总图」与「状态表」登记该文档,并注明
与锁定版冲突时以落地版为准。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 22:55:01 +08:00
1445043649 dd1d40368d feat(scripts): 新增本地调试与示例启动脚本,并登记进 apps/README
新增 4 个脚本,统一子命令风格(start/stop/restart/status/logs/run):

- scripts/dev-gateway.sh:方式 B,只起 Gateway(:8001),用 backend/.venv 虚拟环境,
  自动加载 .env、释放端口、等待就绪;带 PID/日志文件,可查状态与跟随日志。
- scripts/dev-full.sh:方式 A,复用 serve.sh 守护模式起全量栈(Gateway+前端+nginx),
  补齐 serve.sh 缺失的 status 与 logs;restart 默认跳过依赖安装,统一入口 :2026。
- apps/examples/http-chat/run.sh:自动探测网关(:2026 优先,回退 :8001),
  优先 uv 临时环境带 requests(--no-project,不污染系统),无 uv 时回退 venv+pip。
- apps/examples/embedded-chat/run.sh:自动定位 backend、加载 .env 后用 uv run 运行,
  内嵌 SDK 模式无需起服务。

apps/README.md 增加「本地调试脚本」与「运行示例」两节,说明上述脚本用法。

全部脚本均已本地实跑验证:全量栈三服务 HTTP 200,两个示例多轮对话正常。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 22:47:30 +08:00
1445043649 7dfc9968fe feat(apps): 新增 DeerFlow 应用脚手架,并修复 store 的 database 回退
apps/:新增「基于 DeerFlow 的应用」目录,与 backend/、frontend/ 平级,
位于「app 消费 deerflow、不反向依赖」边界的正确侧。含两种集成示例:
  - examples/http-chat   —— HTTP Gateway (REST+SSE),含登录/CSRF/建线程/流式对话
  - examples/embedded-chat —— 进程内直接调 DeerFlowClient
README 说明边界规则、两种模式、鉴权流程及新建应用约定。

runtime/store:修复 make_store 缺失的 database 段回退。原先 store 工厂只读
legacy 的 checkpointer 段,导致仅配 database:postgres 时,checkpointer 走了
Postgres、但 store 仍回退 InMemoryStore(并打出误导性的「线程列表会丢失」告警,
实际线程在 threads_meta 表里、本就持久)。现对齐 checkpointer 工厂的优先级:
checkpointer 段 → database 段 → InMemoryStore;postgres 分支同样剥掉 +asyncpg
方言前缀,使一个 DATABASE_URL 同时满足 SQLAlchemy 与 LangGraph 的 psycopg store。
告警文案也修正为「跨线程 store 数据会丢失」。

tests:新增 test_store_provider.py(3 例,TDD)覆盖 database→postgres 回退、
无配置时的内存回退、以及 checkpointer 段优先级。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 22:02:44 +08:00
1445043649 84de632b19 chore(scripts): add verify_stage0.sh — systematic Stage 0 verification
Adds a single script that walks the 5 verification layers in order
and emits pass/fail counts at the end:

  static   — boundary scans + full pytest (baseline ≥ 3250 passed,
             ≤ 18 fails matching known caplog flake set) + ruff lint
  paths    — .deer-flow/users/ should be empty (or absent);
             workspaces/{wid}/threads/{tid}/... layout in place;
             exercises `make migrate-paths DRY_RUN=1`
  rds      — PG reachable; alembic at 0003; service_accounts +
             api_keys + external_users tables present;
             idx_api_keys_active partial index has
             "WHERE revoked_at IS NULL" predicate; workspace_id is
             NOT NULL on thread_meta / runs / feedback / run_events;
             UNIQUE(workspace_id, thread_id) on thread_meta
             (requires DATABASE_URL; skipped if unset)
  runtime  — curl /health on the Gateway (requires `make dev`;
             skips downstream e2e if unreachable)
  e2e      — register two users via /api/auth/register, capture each
             session's csrf_token, create one thread per user,
             cross-access GET + DELETE both return 404 (per PR6
             "404 not 403" contract), same-workspace GET returns 200

Each layer is independently runnable: `./verify_stage0.sh paths rds`.
With no args runs all five.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:54:02 +08:00
1445043649 6090b9e5b1 docs(impl): PR8 implementation note + STATUS update (Stage 0 收尾)
Adds `pr8-headless-api-schema.md`:
- Scope summary (3 schema-only tables for Stage 1 headless API base)
- Per-task commit table (T8.1-T8.6)
- Acceptance: 9 new tests + 3 tables auto-created + FK behaviour + partial index DDL pinned
- Architecture decisions (key_prefix global UNIQUE rationale, String scopes not PG text[], external_users workspace_id redundancy, no Repository class until Stage 1, T8.6 reverse invariant)
- File structure index (new vs modified)
- Live smoke commands (RDS \dt + \d+ api_keys for partial index)

Updates STATUS.md:
- One-line status: Stage 0 工程层面收尾 (PR1-PR8 全合)
- 8-PR status table: PR8 row marked merged with commit range and impl note link
- Test baseline: PR8 末 3250 passed + 31 skipped + 18 flake; +163 new tests over Stage 0
- Skipped/deferred 行 加 PR8 RDS live smoke 项
- Next-step suggestion 翻新:6 个 live verification 用户跟进项 + Stage 0 退出 Go/No-Go 工程门已满足 + Stage 1 可启动方向
- PR8 经验回顾段:Inline + 严格 TDD,3 张表互相独立,T8.6 反向 invariant 锁定历史坑

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:18:18 +08:00
1445043649 f803f393d3 test(persistence): PR8 T8.6 — verify all 3 PR8 tables auto-created
Adds tests/test_pr8_metadata_registration.py: opens a fresh SQLite
engine via init_engine() and asserts inspect(conn).get_table_names()
contains service_accounts, api_keys, and external_users. Guards
against an ORM row class being added under deerflow/persistence/* but
accidentally left out of deerflow/persistence/models/__init__.py —
which would leave the table un-provisioned at startup and surface as
a confusing "no such table" later in Stage 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:14:14 +08:00
1445043649 6f806ff4a4 feat(persistence): PR8 T8.5 — ExternalUserRow ORM + 2 tests
Adds deerflow/persistence/external_user/{__init__,model}.py with
ExternalUserRow:
  - id: UUID36 PK
  - workspace_id: FK workspaces ON DELETE CASCADE (redundant with SA's
    workspace_id but stored directly to speed workspace-scoped queries
    that span multiple SAs)
  - service_account_id: FK service_accounts ON DELETE CASCADE
  - external_id: String(128) — caller-supplied X-External-User-Id
  - display_name: String(128) nullable (admin UI only, not auth-relevant)
  - metadata_json: JSON nullable=False default {} — plan tier / region /
    custom tags
  - created_at / last_seen_at (UTC)
  - UniqueConstraint (service_account_id, external_id)
    name=uq_external_users_sa_external — the same external_id may be
    reused under a different SA, but is upsert-unique under a single SA

T8.5 tests:
  - test_unique_service_account_id_plus_external_id: second row with
    same (SA, external_id) raises IntegrityError
  - test_cascade_on_service_account_delete: deleting parent SA removes
    all external_users rows under it

Registered in deerflow/persistence/models/__init__.py — all three PR8
tables (service_accounts / api_keys / external_users) are now wired
into Base.metadata.create_all().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:12:43 +08:00
1445043649 52e9999a61 feat(persistence): PR8 T8.4 — ApiKeyRow ORM + 3 tests
Adds deerflow/persistence/api_key/{__init__,model}.py with ApiKeyRow:
  - id: UUID36 PK
  - service_account_id: FK service_accounts ON DELETE CASCADE
  - key_prefix: String(16), UNIQUE — global uniqueness preserved even
    after revoke so audit logs never reference an ambiguous prefix
  - key_hash: String(128) sha-256 hex (plaintext returned only once at
    create time)
  - name / scopes / rate_limit_rpm / expires_at / last_used_at /
    revoked_at — all nullable or default-providing
  - created_at: UTC
  - Index idx_api_keys_sa (service_account_id) — list keys for an SA
  - Index idx_api_keys_active (key_prefix) WHERE revoked_at IS NULL —
    partial index, dual-dialect via sqlite_where + postgresql_where,
    shrinks the hot-path lookup index by excluding revoked keys

T8.4 tests:
  - test_unique_key_prefix_enforced: column-level UNIQUE blocks two
    rows from sharing key_prefix (active or revoked alike)
  - test_active_index_declares_both_dialect_where_clauses: schema
    introspection confirms idx_api_keys_active has both
    `dialect_options.sqlite.where` and `dialect_options.postgresql.where`
    set to `revoked_at IS NULL` — guards against accidental loss of the
    dual-driver hint when the Index is edited later
  - test_cascade_on_service_account_delete: deleting the parent SA
    removes all api_keys rows

Registered in deerflow/persistence/models/__init__.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:10:32 +08:00
1445043649 bb7289781e test(persistence): PR8 T8.2 + T8.3 — ServiceAccount cascade / restrict
T8.2 test_cascade_on_workspace_delete: seed user → workspace → SA,
delete workspace, assert SA row is gone (ondelete CASCADE on
workspace_id FK).

T8.3 test_restrict_on_created_by_user_delete: seed user → workspace
→ SA, then attempt DELETE FROM users WHERE id = created_by, assert
IntegrityError raises and the SA row survives (ondelete RESTRICT on
created_by FK).

SQLite enforces FKs because the engine's connect-listener turns on
`PRAGMA foreign_keys = ON` for every new connection — see engine.py
init_engine().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:07:16 +08:00
1445043649 1fb07e48e6 feat(persistence): PR8 T8.1 — ServiceAccountRow ORM + insert smoke
Adds deerflow/persistence/service_account/{__init__,model}.py with
ServiceAccountRow:
  - id: UUID36 PK
  - workspace_id: FK workspaces ON DELETE CASCADE
  - name: String(64)
  - role: String(16) default "member" (Stage 0 lone value; Stage 2 RBAC)
  - identity_mode: String(16) default "collapsed" — three states
    "collapsed" / "external_passthrough" / "both"; Stage 1 API key
    auth layer branches on this to decide whether each call writes
    an external_users row
  - status: String(16) default "active"
  - created_by: FK users ON DELETE RESTRICT (must hand off / delete
    SAs before removing their creator)
  - created_at / updated_at (UTC, onupdate)
  - Index idx_service_accounts_workspace (workspace_id, status)

Registers in deerflow/persistence/models/__init__.py so the engine's
side-effect import path picks it up for Base.metadata.create_all().

T8.1 test: insert_smoke writes a row through a session, reads it back,
asserts each column round-trips. SQLite ephemeral DB per test via
tmp_path, no Postgres required at this layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:06:21 +08:00