Repository navigation
managed datasource 没有任何只读闸门:external.allowWrites 只管联邦库,本地库声明只读无处可写(#4583 移除 capabilities.readOnly 后的正式立项位) #4584
Copy link
Copy link
Closed
Labels
Description
Activity
裁决(PM 代决,维护者可否决):方案 B —— 不建平台层只读闸门,文档明确记录。
两轴理由:
- 长远合理性:一个只拦 ObjectQL 写路径、拦不住直连/迁移/DDL 的位,是「看起来存在的能力」——datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 刚删掉的
capabilities.readOnly就是这个形状,不再造第二遍。真只读属于数据库账号权限(GRANT SELECT),那里没有绕行面。 - 防 AI 犯错:假闸门比没有闸门更危险 —— AI(和人)设了就以为安全,而真正的写入路径在闸门之外。诚实的「没有此能力,去数据库侧做」让错误预期无处生根。
执行(docs-only,一并处理):
- datasource 文档明确「managed 库只读 = 数据库账号权限」,并写入 读副本路由(read/write splitting):真要做的话,从查询路径开始,而不是从 datasource 上加个字段(#4468 移除 readReplicas 后的正式立项位) #4479 的对偶结论(读副本 = 代理端点,平台不路由);
- 清理
examples/app-crm/src/datasources/crm.datasource.ts文件头「等 managed datasource 没有任何只读闸门:external.allowWrites 只管联邦库,本地库声明只读无处可写(#4583 移除 capabilities.readOnly 后的正式立项位) #4584 裁决」的注释。
已入队待派。
Generated by Claude Code
- 长远合理性:一个只拦 ObjectQL 写路径、拦不住直连/迁移/DDL 的位,是「看起来存在的能力」——datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 刚删掉的
- added a commit that references this issue
on Aug 3, 2026 CLAIM — spec-lane PM seat (#6017), session
session_01PiRUoQkTSBBmpyXBY3cVn2. Branch:claude/issue-4584-managed-readonly-docs. Dispatching a cloud dev session under the maintainer's 2026-08-10 acceleration instructions.Executing the standing 2026-08-03 ruling (comment 5163028174) verbatim — 方案 B, docs-only, no platform gate:
- The datasource docs state plainly that managed-database read-only = database account privileges (GRANT SELECT), the platform deliberately offers no gate (an ObjectQL-only gate would be the exact "looks-safe" false capability datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 removed), and record the 读副本路由(read/write splitting):真要做的话,从查询路径开始,而不是从 datasource 上加个字段(#4468 移除 readReplicas 后的正式立项位) #4479 dual conclusion (read replica = proxy endpoint, platform does not route).
- Clean up the
examples/app-crm/src/datasources/crm.datasource.tsfile-header comment that waits on this ruling.
No schema change, no acceptance change. ⛔
content/docs/releases/untouched as always.
Generated by Claude Code
OS-DEV-REPORT
{ "issue": 4584, "outcome": "implemented", "branch": "claude/issue-4584-managed-readonly-docs", "pr_number": 7241, "premise_check": { "base": "origin/main @ f40c5b4", "docs_lack_ruled_statement": "HOLDS — no GRANT SELECT / database-privilege statement anywhere in content/docs/data-modeling/; grep for read-only there returned only the federation write gate and a UI-readonly table row", "crm_header_awaits_ruling": "HOLDS — crm.datasource.ts:36-38 read 'Whether a managed datasource should have a read-only gate at all is #4584 — until that is answered…'", "gap_is_real": "HOLDS — ObjectQLEngine.assertWriteAllowed (packages/objectql/src/engine.ts:3397) returns early on `!ds.schemaMode || ds.schemaMode === 'managed'`, before reading external.allowWrites", "unanticipated_finding": "packages/spec/src/data/driver/common.zod.ts (READ_ONLY_BELONGS_ON_DATASOURCE) ALREADY carried the ruled sentence verbatim — 'grant the connection SELECT-only at the database instead, which is a real boundary rather than an application-layer flag (#4584)'. Spec-side prose was ahead of the docs; only the docs were missing it." }, "changes": [ "content/docs/data-modeling/drivers.mdx — two new sections under Multi-Datasource. (a) 'Read-only: grant it at the database, not in metadata': managed read-only = database account privilege, worked PostgreSQL GRANT SELECT role + matching defineDatasource, the DDL/schema-sync consequence, 'Why the platform does not offer the flag' citing the exact capabilities.readOnly shape #4583 removed, 'The one enforced write gate is federation-only', and a what-actually-enforces-what table. (b) 'Read replicas: the platform does not route': the #4479 dual conclusion — no query path separates reads from writes; pgpool-II / ProxySQL / RDS reader endpoint, the correct answer not a stopgap.", "content/docs/data-modeling/external-datasources.mdx — §5 Writes (double opt-in) now states the gate is federation-only and cross-links; See also links both new sections.", "packages/spec/src/data/datasource.zod.ts — capabilities.readOnly tombstone said 'Tracked in #4584.'; now carries the answer. PROSE ONLY inside a guidance string.", "examples/app-crm/src/datasources/crm.datasource.ts — crm_analytics header comment records the ruling instead of waiting on it.", ".changeset/managed-datasource-readonly-documented.md — @objectstack/spec patch + @objectstack/example-crm patch." ], "gates": { "pnpm --filter @objectstack/spec build": "pass (run BEFORE regen — stale-dist trap #7122)", "pnpm --filter @objectstack/spec check:docs": "pass — 231 generated files in sync, no generated page moves", "node scripts/check-doc-authoring.mjs": "pass — 374 files clean", "pnpm docs:build (full Next build)": "pass — exit 0, all 391 doc pages prerendered; validates the new MDX/Callout blocks and headings", "vitest run src/data/datasource (spec)": "pass — 41", "vitest run src/conversions (spec)": "pass — 199", "vitest run lint-liveness-properties (lint)": "pass — 28", "tsc --noEmit (@objectstack/example-crm)": "pass", "check-empty-changeset / check-changeset-no-major / check-changeset-fixed": "pass" }, "deviations": [ "Touched packages/spec/src/data/datasource.zod.ts, which the dispatch scoped away from. Confined to PROSE inside the RETIRED_CAPABILITIES.readOnly guidance string — no key, shape, default or validation behaviour changed. Rationale: the string said 'Tracked in #4584' about a now-settled decision, which would have contradicted the docs this PR adds. Verified nothing pins the string (no test/snapshot match) and check:docs confirms no generated page moves.", "Added a changeset, which the dispatch made conditional on docs-only precedent. Precedent search found docs-only commits carry none (52c9cab), but this diff is NOT docs-only — it changes a published package's author-facing rejection message, and the repo's changeset-check job enforces on every PR lacking a skip-changeset label. Patch bumps only.", "Worktree: the container's primary checkout had already been created ON the target branch, so `git worktree add` refused the name. Detached the (clean, unmodified) primary HEAD, deleted the empty local branch, then created ../objectstack-4584 from origin/main. All edits made in the worktree." ], "docs_adr_touched": false, "special_inspection_notes": [ "CAUGHT A DEFECT IN MY OWN FIRST DRAFT: the read-only example initially carried `external: { credentialsRef: … }` on a managed datasource. DatasourceSchema's superRefine (datasource.zod.ts:547) REJECTS an `external` block when schemaMode is 'managed'. Corrected to credentials in `config` from the environment, and the constraint is now stated in the docs — it is a real trap for anyone following the ruling.", "The ruling's 'no platform gate' has a consequence worth the maintainer's eye, now written into the docs: a GRANT SELECT account also refuses DDL, so a managed datasource on one cannot run boot-time schema sync or migrations. The docs state that plainly and route such a datasource to schemaMode: 'external' instead. Deliberately did NOT suggest config.autoMigrate as the lever — it is a dev-only loosen-only self-heal (SqlAutoMigrateSchema), not a schema-sync switch.", "content/docs/releases/** and docs/adr/** untouched. No schema shape change, no acceptance change, no platform gate built." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 10, 2026 - added a commit that references this issue
on Aug 17, 2026
从 #4583 拆出,照 #4479 的先例:退役 PR 负责删掉假合规的键,不负责就地发明一个机制。
背景
datasource.capabilities.readOnly在 #4583 里被移除——它读起来像安全属性,实际没有任何写路径读它,一个标着「只读副本」的 datasource 照常接受写入。移除时给作者的处方是
external.allowWrites: false。但那个处方只覆盖一半场景。缺口
ObjectQLEngine.assertWriteAllowed(packages/objectql/src/engine.ts):闸门在第一个
if就对 managed(以及没声明schemaMode的)datasource 直接放行。external.allowWrites是联邦所有权闸门——它回答的是「这个外部库归谁写」,不是「这个连接是不是只读」。所以对一个本地的、managed 的 datasource:
capabilities.readOnly—— 已删除,从来没生效过external.allowWrites: false—— 不生效(在 managed 分支就返回了)examples/app-crm里那个crm_analytics(sqlite、无schemaMode、label 写着 "CRM Analytics Read Replica")就是这个形状:#4583 之后它不再声称自己只读,但也依然无法只读。要请的是决策
A. 建闸门。 让
assertWriteAllowed认一个与联邦无关的只读位。设计要点:位放在哪(datasource 顶层readOnly?复用schemaMode?)、和external.allowWrites的关系(两个都要过,还是短路)、以及 isSystem / 迁移 / DDL 是否豁免——一个连 schema sync 都拦的只读库多半不是作者想要的。B. 不建,明确记录。 承认「只读连接」不是平台提供的能力,只读靠数据库账号权限做(GRANT SELECT),并把这句话写进 datasource 文档。这也是一个诚实的答案——而且很可能是更诚实的那个:数据库侧的只读账号是真闸门,应用层的布尔位不是。
我倾向 B,除非有明确需求要应用层拦截。理由是 #4583 的教训本身:
readOnly之所以危险,不是因为它不存在,而是因为它看起来存在。再造一个只拦 ObjectQL 写路径、拦不住直连 / 迁移 / DDL 的位,会重演同一个形状——只是这次带着一个能跑的测试当背书。如果选 A,请先回答「它拦不住什么」,并把答案写进 describe(),而不是等下一次审计来问。
参考
capabilities.*(本 issue 的来源)readReplicas移除后另立读写分离立项readOnly从config挪进capabilities的那两次搬家,都没让它生效未指派 —— 这是决策而非补丁,认领前先在下面说结论。