Skip to content

Correct the AGENTS.md claims the @objectstack 18 upgrade made false - #157

Draft
os-warren wants to merge 1 commit into
mainfrom
claude/project-thread-blpcmy
Draft

os-warren wants to merge 1 commit into
mainfrom
claude/project-thread-blpcmy

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Requested by Warren · project thread

Fixes #156
Clause-②: no
Metadata-protocol: none

维护者速读(草稿)

  • 改了什么:只改 AGENTS.md 三处说明,代码、元数据、测试一行没动。①「哪一层拒绝只读字段」那段改写为 18.2 的真实行为;② 种子数据示例里给 sys_user 写的 username 换成 email;③ 视图自定义覆盖层的存放范围从「按组织」改为「整个环境」。
  • 为什么改:升级到 objectstack 18 后这三句话不再成立。照旧文档写种子数据,用户那一行会被拒收;按旧说法理解只读保护和视图覆盖层范围,也会做出错误判断。三处都在 18.2 上实测或对照源码核实过。
  • 风险与回滚:纯文档,不影响运行中的应用。回滚只需 revert 这一个提交。
  • 先看:AGENTS.md 的「Which layer refuses」一节(风险最大,整段重写),然后是种子示例的 sys_user 那两行,最后是「Who can customize a view」第一段。
  • 两分钟验证:看 diff 即可:改动只落在 AGENTS.md,共 3 处 hunk,文中没有其它改动。
  • 需要你做的:确认措辞后合并本 PR(受管路径,由你合并)。
  • 席位意见:

What changes

AGENTS.md only, three hunks. Each old and new sentence is quoted below, with what it was checked against.

1. The readonly-layer caveat (the "One caveat about which layer refuses" section)

Old:

The insert-path readonly strip is a boundary guard: it lives in MetadataProtocolService.createData, which is what @objectstack/rest — and so REST, OpenAPI and MCP — writes through. engine.insert applies no readonly strip at all (the update path is different: that strip is inside ObjectQL and is isSystem-gated). So a direct data.insert from in-process code can set a readonly column at insert with no context and no warning.

Filed upstream as objectstack-ai/objectstack#14147, and pinned as a tripwire in test/seed-history.test.ts so it goes red when the platform closes it. Practical consequence for authoring here: do not read an engine-level test as proof that a column is protected — assert against protocol.createData when the refusal is the thing you care about. Flows are unaffected today because assignment.flow.ts declares runAs: 'system', which is elevated regardless.

New (heading now "Which layer refuses"):

Both verbs strip a non-system caller's readonly value inside ObjectQL, and isSystem is the exemption on both. On insert the strip runs in engine.insert after the beforeInsert hooks, so a key a hook assigned is kept. The stripped write still succeeds: the column takes its default, the engine logs a warning and reports the drop (reason: 'readonly'); it does not throw.

(an 18.2.0 measurement table: no context / isSystem: false / isSystem: true)

So an engine-level test now proves the same refusal protocol.createData does. Through 17.3 it did not: the insert strip lived only at the API boundary, and engine.insert kept the value (objectstack#14147). 17.4 moved it into the engine; the tripwire test/seed-history.test.ts carried went red on the 18 upgrade and was deleted, as it asked.

Checked against:

  • Source, @objectstack/objectql 18.2.0 dist/index.js. insert() runs staticReadonlyCreateStrip (calling stripReadonlyFields(..., { verb: "insert", hookWrittenKeys })) inside if (!opCtx.context?.isSystem), after the post-hook data is collected. @objectstack/metadata-protocol 18.2.0 says it in so many words: "Every create-side strip is the ENGINE's — runtime-owned autonumber (#5503) and static author-declared readonly (#14147)". The drop warning reads "the caller-supplied value was DROPPED and the create is being COMMITTED WITHOUT IT — the call returns success while this column takes its declared defaultValue".
  • Version of the move. The objectql CHANGELOG, ## 17.4.0, b398ad2: "BREAKING (behaviour): a static readonly field is now stripped from a non-system caller's INSERT payload inside engine.insert". It also deleted stripReadonlyForInsert from metadata-protocol.
  • Measurement. A throwaway vitest on this branch, using the same kernel as test/seed-history.test.ts and deleted afterwards. It ran data.insert('duly_task', { status: 'open', completed_at, … }, { context, onFieldsDropped }):
Context completed_at read back onFieldsDropped
none null {"object":"duly_task","fields":["completed_at"],"reason":"readonly"}
{ isSystem: false } null same
{ isSystem: true } 2026-05-04T09:00:00.000Z not called
  • The deleted tripwire: test/seed-history.test.ts's header (from Upgrade to @objectstack 18.2.0 #144, c20d397) records the same history.
  • The assignment.flow.ts sentence is dropped. It explained why flows escaped the hole, and the hole no longer exists.

2. username in the seed example

Old:

  records: [{ name: 'seed_user_alice', username: 'seed_user_alice', /* … */ }] },

New:

//    sys_user has no `username` column: from @objectstack 18 the loader refuses
//    a row carrying one — "Unknown field 'username' on object 'sys_user'".
  records: [{ name: 'seed_user_alice', email: 'alice@example.invalid', /* … */ }] },

Checked against:

  • @objectstack/platform-objects 18.2.0 sys_user fields. There is no username ('username' in fields gives false), and email is required: true. The new example matches test/seed-history.test.ts's own userSeed ({ name: 'seed_user_alice', email: 'alice@example.invalid' }).
  • Measurement. The same throwaway kernel seeded two sys_user rows, one with username. The loader logged Failed to write sys_user record #1 (name=seed_user_bob): Unknown field 'username' on object 'sys_user' (thrown by objectql undeclaredWriteFieldErrors), and only seed_user_alice landed. A direct system data.insert with username threw INVALID_FIELD.

3. The org-scoped view overlay wording (around line 241): measured, then corrected

Old:

stored in sys_metadata as scope: platform, owner: null, org-scoped, replacing the declared view for the whole organisation

New:

stored in sys_metadata as scope: platform, owner: null, environment-wide — the row has no organization column, and 18.2 answers Saved customization overlay (env-wide, …) — replacing the declared view for every organisation in the deployment

Measured against a live pnpm demo on this branch (@objectstack 18.2.0), signed in as the demo admin (platform admin):

  • PUT /api/v1/meta/view/duly_task.default with sort changed answered 200 with "message":"Saved customization overlay (env-wide, state=active) — type=view, name=duly_task.default [seq=1]". On 17.2 the answer was (org=…).
  • sys_metadata went from total: 0 to 1. The row is scope: "platform", owner: null, environment_id: null, package_id: null, state: "active".
  • The sys_metadata object's field list (GET /api/v1/meta/object/sys_metadata) has no organization_id.
  • A follow-up GET of the view returned the new sort, so the overlay replaces the declared view.
  • This matches ADR-0131 D6: "an environment row of the same name replaces the managed item whole — ADR-0005's environment layer, exactly as today, with the per-organization axis retired".

Tests

Docs-only. The four gates ran on the tree committed as 98a0c23, with the scratch probe already deleted:

Gate Exit Reading
pnpm validate 0 warnings only (pre-existing field-no-consumers)
pnpm typecheck 0
pnpm test 0 Test Files 38 passed (38) · Tests 916 passed (916)
pnpm build 0 dist/objectstack.json (234.3 KB)

Acceptance notes

  • Left alone, on purpose. The 401/403/200 table under "Who can customize a view" carries its own label, "Measured on @objectstack/rest 17.2.0", and its 200 row quotes the 17.2 response (org=…). It is a dated historical measurement and stays true as labelled, so the card's "change nothing else" keeps it. If the seat wants it re-measured on 18.2, that needs a new card.
  • NOT MEASURED: the manage_metadata gate for the three Duly sets on 18.2. reason: the demo refuses self-registration (POST /api/v1/auth/sign-up/email answered 403), so the three one-set accounts the 17.2 table used could not be created. @objectstack/rest 18.2.0 still has the gate as source ("Rewriting stored metadata requires the manage_metadata capability.", skipped only for isSystem). The sentence that names it was not touched. Anonymous PUT answered 401 UNAUTHENTICATED, the same as 17.2.
  • The demo writes an overlay row into sys_metadata under .objectstack/data in my local worktree only. Nothing is committed.

Verification

  • git diff origin/main --stat lists AGENTS.md | 47 +++++++++++++++++++++++++++++------------------, one file and three hunks.

Generated by Claude Code

- "Which layer refuses": on 18.2 both verbs strip a non-system caller's
  readonly value inside ObjectQL; engine.insert no longer keeps it, and the
  seed-history tripwire the old caveat cited is gone. Measured on 18.2.0.
- Seed example: sys_user has no `username` column and 18 refuses a row that
  carries one; the example now writes `email`, as the test fixture does.
- View overlay: the stored overlay is environment-wide on 18.2 (no
  organization column, response says `env-wide`), not org-scoped.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbaHbCfrMnaJ8UT44ob
@os-warren os-warren added documentation Improvements or additions to documentation needs-user-decision labels Nov 2, 2026 — with Claude
@os-warren os-warren self-assigned this Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor

维护者速读

Seat record for this PR, 2026-11-02T02:31Z. The PR changes AGENTS.md, which is a governed path (Tier H). The seat reviews it and does not merge it; it stays draft with needs-user-decision until the maintainer merges it. This is the seat's final version of the draft in the body, checked against the diff, with the seat opinion filled in.

改了什么: 只改 AGENTS.md 的三处说明,代码、元数据和测试都没动。

  1. 「哪一层拒绝只读字段」一节整段重写。现在写的是:在 18.2 上,新建和修改两条路径都在引擎内部丢弃非系统调用者写的只读字段,并附实测表。
  2. 种子数据示例里,sys_user 的 username 换成 email,因为 18 已经拒收 username。
  3. 视图自定义覆盖层的生效范围,从“按组织”更正为“整个环境”。

为什么改: 升级到 18 以后,这三句话都不对了。照旧文档写种子数据,那一行会被拒收;按旧说法理解只读保护和视图覆盖层的范围,开发者(包括 AI)会做出错误判断。三处都在 18.2 上实测过,或对照源码核实过。

风险与代价(含回滚): 纯文档改动,不影响运行中的应用。回滚只需 revert 这一个提交。两处没有改:一张注明「17.2 实测」的旧表保持原样,因为它是带日期的历史读数;各权限集的 manage_metadata 403 这次没有在 18.2 上实测,原因是 demo 拒绝注册新账号,但源码里的拦截仍在。

席位意见: 建议合并。diff 只有 AGENTS.md 一个文件、3 处改动,与 PR 正文逐句引用的新旧对照一致。新句子要么有 18.2 实测,要么有源码出处。verify 在 98a0c23 上是绿的。

你要做的(一个动作): 确认措辞后合并本 PR,并在合并前把它从 draft 转为 ready。受管路径由你合并,席位不合。

席位核对

ACCEPT (Tier H, waits for the maintainer) — PR #157 at head 98a0c23279d0c71c177ec6417a9cd126b4cfc9dd (duly lane seat, session session_011HyoSXGRy6L13mYq1Hp4n1). Implemented by the #156 card thread session_01JQwxbaHbCfrMnaJ8UT44ob.

  • Shape: draft, base main 25743ed, Fixes #156, Clause-②: no, Metadata-protocol: none, labels needs-user-decision and documentation, assignee os-warren. The author is os-warren, so no review request can be sent; the assignment is the ask.
  • Files (API list): AGENTS.md only, +29/−18, in three hunks (≈line 241 overlay scope, the ≈line 341 seed example, and the "Which layer refuses" section), matching the Claim surface.
  • Gate: check-run verify success on 98a0c23.
  • Premise measured:
    • The readonly strip is in engine.insert on 18.2: no context and isSystem: false store null and report readonly, while isSystem: true keeps the value.
    • The seed loader refuses username on sys_user.
    • The overlay PUT answers "env-wide", and sys_metadata has no organization column.
  • Accepted as noted: the dated 17.2 table is left alone, and the per-set manage_metadata 403s on 18.2 are NOT MEASURED, because sign-up is refused in the demo. The source gate is still present.
  • Landing: Tier H, so the seat does not flip ready and does not merge.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Seat note at 2026-11-02T06:39Z (duly lane seat, session session_011HyoSXGRy6L13mYq1Hp4n1): two more AGENTS.md claims have gone stale since this PR was opened. They are listed here so the maintainer can take them in this PR or in a follow-up.

  1. Rule 5, "late_after … stamped at dispatch". Since Apply the duty's grace to tasks created by hand on a duty #173 (98c188a) the stamp is set at creation: by the planner for a dispatched task, and by duly_task_lifecycle_stamps for every other task that carries a duty (its duty's grace, read with runAs: 'system').
  2. "One caveat about which layer refuses". It still says engine.insert applies no readonly strip (#14147). On 18.2 the engine strips static readonly fields on insert too, after beforeInsert and under the same isSystem gate as update, and re-derives each field's defaultValue (ruling C on objectstack#14147). This was measured by the A member can write source, owner and business_unit on their own task, and so enter governed readings or someone else's #176 card thread (project file duly-176-task-provenance/step0.md).

The seat does not push to this PR. It is governed and waits on the maintainer.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AGENTS.md: correct two claims the @objectstack 18 upgrade falsified (readonly-layer caveat, username in the seed example)

2 participants