From 30ff9f47cd086a155de769ecf2b1f5e2ebe9487e Mon Sep 17 00:00:00 2001 From: "objectstack-fleet[bot]" Date: Tue, 27 Oct 2026 01:25:57 +0000 Subject: [PATCH 1/2] docs(skills): objectstack-data teaches the field permission `when` and when to pick it over a validation rule `rules/security.md` gains a section on the row-conditioned edit lock: the choice rule (a lock for everyone is a validation rule; a lock that depends on who edits is `when` on that set's field entries, never both), the contract example, the four boundaries (edit vote only, same object, pre-image on update, locks and never grants) and the docs pointers. `SKILL.md`'s `readonlyWhen` line names the per-role alternative inside the file's headroom. One eval case pins the contract prompt. The two touched token-ratchet rows are raised by the measured remainder under the maintainer's ruling quoted beside them. Claude-Session: https://claude.ai/code/session_01JQwxbakp4nDuCkULsj7Fyr Co-authored-by: Claude --- scripts/check-skills-token-ratchet.mjs | 21 ++++++++++++-- skills/objectstack-data/SKILL.md | 3 +- .../hooks-security-seeds-datasources.json | 10 +++++++ skills/objectstack-data/rules/security.md | 29 +++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 2811c044162..3a72f9f3d9a 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -552,7 +552,16 @@ export const CEILINGS = new Map([ // 2026-10-24 (project decision list item 9), verbatim and untranslated, the // line for item 8 of the same message elided: // 「9. 技能文档 security.md 的 token 上限 2543 → 2563 … 同意」. - ['skills/objectstack-data/rules/security.md', 2563], + // 2563 -> 2957 (#26179): the field permission row condition `when` — the + // choice rule (a lock for everyone is a validation; a lock that depends on + // who edits is `when` on that set's field entries), one example and the + // four boundaries — which the file did not teach, so an author locked the + // fields for every user. +396 tokens on a landed 2561, 2 absorbed by + // headroom, ceiling +394. The file carries no duplicated passage to cut + // first (measured: no sentence repeats in the file). Maintainer ruling + // 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天 + // 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」. + ['skills/objectstack-data/rules/security.md', 2957], // 3024 -> 3109 (2026-08-31 app-repo-principles raise, see the block above). // Severity Levels listed the three values and left the CHOICE unstated: a // block rests on a judgement a person made, so a machine-inferred signal — a @@ -651,7 +660,15 @@ export const CEILINGS = new Map([ ['skills/objectstack-ai/evals/skills-tools-knowledge.json', 1074], ['skills/objectstack-api/evals/endpoints-auth-routes.json', 1042], ['skills/objectstack-automation/evals/flows-triggers-approvals.json', 1255], - ['skills/objectstack-data/evals/hooks-security-seeds-datasources.json', 894], + // 894 -> 1095 (#26179): one regression case for the field permission `when` + // (the contract prompt: lock the legal terms for finance from approval on, + // legal keeps the edit) — expected: `when` on the finance set's entries with + // `readable: true`, and NO validation rule or `readonlyWhen` locking the + // fields for every user. +201 tokens, zero headroom to absorb any of it, + // ceiling +201; nothing duplicated in the file to cut first. Same ruling as + // the `rules/security.md` row above (2026-10-27, tap A: + // 「A:先削重复,余额按实测提高」). + ['skills/objectstack-data/evals/hooks-security-seeds-datasources.json', 1095], ['skills/objectstack-data/evals/objects-fields-relationships.json', 946], ['skills/objectstack-formula/evals/cel-predicates-formulas.json', 1046], ['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1004], diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 468a5b3fd22..2e446dc5e54 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -218,7 +218,8 @@ export const Invoice = ObjectSchema.create({ - Use `visibleWhen` to hide irrelevant fields in ObjectUI forms. - Use `readonlyWhen` for state-locked fields; the ObjectQL write path ignores - incoming changes when the predicate is `TRUE`. + incoming changes when the predicate is `TRUE` (for everyone; per role → + field permission `when`, rules/security.md). - **`readonly: true` governs the end-user surface, not trusted system writers.** A non-system write (REST/UI, and any `runAs:'user'` flow — the default) has the field **stripped** from any non-system write; the write reports diff --git a/skills/objectstack-data/evals/hooks-security-seeds-datasources.json b/skills/objectstack-data/evals/hooks-security-seeds-datasources.json index 6f4648942de..42a526f0eae 100644 --- a/skills/objectstack-data/evals/hooks-security-seeds-datasources.json +++ b/skills/objectstack-data/evals/hooks-security-seeds-datasources.json @@ -40,6 +40,16 @@ "must_contain": ["defineSeed", "externalId", "upsert", "env:", "daysFromNow"], "must_not_contain": ["new Date(", "defineDataset", "externalId: 'id'"] } + }, + { + "id": 5, + "prompt": "Finance may edit a contract's legal terms (`governing_law`, `liability_cap`) while it is a draft, but not once it is approved, signing or active. Legal still edits them.", + "expected_output": "Field permission entries with a row condition on the finance set: `'clm_contract.governing_law': { readable: true, editable: false, when: \"status in ['approved', 'signing', 'active']\" }` and the same for `liability_cap`; the legal set keeps its edit grant. No validation rule and no `readonlyWhen`, which would lock the fields for every user.", + "files": [], + "assertions": { + "must_contain": ["when:", "editable: false", "readable: true", "clm_contract.governing_law"], + "must_not_contain": ["validations:", "readonlyWhen"] + } } ] } diff --git a/skills/objectstack-data/rules/security.md b/skills/objectstack-data/rules/security.md index a6fed439cf4..5f3085c60a2 100644 --- a/skills/objectstack-data/rules/security.md +++ b/skills/objectstack-data/rules/security.md @@ -116,6 +116,35 @@ bridge (warns). The compiler resolves these `owner_only_deletes`) and the object's `sharingModel` (ADR-0056 D1) over hand-written policies for the common cases. +## Field permissions — a row-conditioned edit lock (`when`) + +A set's `fields` entry (`'OBJECT.FIELD': { readable, editable }`) may carry +`when`, a row condition in the same canonical CEL as `rowLevelSecurity[].using`. +**Choose by who loses the edit — one mechanism per need, never both:** + +- Locked for **everyone** at that stage → a [validation rule](./validation.md) + on the object (`readonlyWhen` likewise strips the write for everyone). +- Locked for **some roles** only → `when` on the field entries of each set + that loses the edit; the other sets keep their grant. + +```typescript +// finance may not change the contract's legal terms after approval +fields: { + 'clm_contract.governing_law': { readable: true, editable: false, when: "status in ['approved', 'signing', 'active']" }, +} +``` + +- **Edit vote only.** Reads are row-blind; `readable: false` beside `when` is + refused at parse time. +- **Same object.** Fields of the entry's object vs a literal or `current_user.*`; + no relation reach (refused at `os validate` / publish, never dropped). +- **Pre-image.** An update is judged on the stored row, so a write cannot unlock + itself by moving `status` back; an insert on the row as sent. +- **Locks, never grants.** On unmatched rows the entry is absent; another held + set's `editable: true` still wins (most-permissive merge). +- Docs: https://objectstack.ai/docs/permissions/field-level-security#row-conditioned-edit-locks-when + and the recipe https://objectstack.ai/docs/permissions/access-recipes#data-lock-fields-for-a-position-once-a-record-reaches-a-stage + ## Sensitive fields — `secret` type + `requiredPermissions` `maskingRule` is **live** (FieldMasker). Two more channels: From 5f62d15bbce8c7b03608b13e7f8390ed9453086f Mon Sep 17 00:00:00 2001 From: "objectstack-fleet[bot]" Date: Tue, 27 Oct 2026 01:35:16 +0000 Subject: [PATCH 2/2] docs(skills): spell the per-set lock without the reserved word `check:role-word` (ADR-0090 D3) refused the new teaching's wording in `rules/security.md` and `SKILL.md`; the choice rule now says "permission set". The security.md ceiling row follows the landed count (2956). Claude-Session: https://claude.ai/code/session_01JQwxbakp4nDuCkULsj7Fyr Co-authored-by: Claude --- scripts/check-skills-token-ratchet.mjs | 8 ++++---- skills/objectstack-data/SKILL.md | 4 ++-- skills/objectstack-data/rules/security.md | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 3a72f9f3d9a..d8148a2e965 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -552,16 +552,16 @@ export const CEILINGS = new Map([ // 2026-10-24 (project decision list item 9), verbatim and untranslated, the // line for item 8 of the same message elided: // 「9. 技能文档 security.md 的 token 上限 2543 → 2563 … 同意」. - // 2563 -> 2957 (#26179): the field permission row condition `when` — the + // 2563 -> 2956 (#26179): the field permission row condition `when` — the // choice rule (a lock for everyone is a validation; a lock that depends on // who edits is `when` on that set's field entries), one example and the // four boundaries — which the file did not teach, so an author locked the - // fields for every user. +396 tokens on a landed 2561, 2 absorbed by - // headroom, ceiling +394. The file carries no duplicated passage to cut + // fields for every user. +395 tokens on a landed 2561, 2 absorbed by + // headroom, ceiling +393. The file carries no duplicated passage to cut // first (measured: no sentence repeats in the file). Maintainer ruling // 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天 // 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」. - ['skills/objectstack-data/rules/security.md', 2957], + ['skills/objectstack-data/rules/security.md', 2956], // 3024 -> 3109 (2026-08-31 app-repo-principles raise, see the block above). // Severity Levels listed the three values and left the CHOICE unstated: a // block rests on a judgement a person made, so a machine-inferred signal — a diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 2e446dc5e54..917b77131f0 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -218,8 +218,8 @@ export const Invoice = ObjectSchema.create({ - Use `visibleWhen` to hide irrelevant fields in ObjectUI forms. - Use `readonlyWhen` for state-locked fields; the ObjectQL write path ignores - incoming changes when the predicate is `TRUE` (for everyone; per role → - field permission `when`, rules/security.md). + incoming changes when the predicate is `TRUE` (for everyone; per permission + set → field permission `when`, rules/security.md). - **`readonly: true` governs the end-user surface, not trusted system writers.** A non-system write (REST/UI, and any `runAs:'user'` flow — the default) has the field **stripped** from any non-system write; the write reports diff --git a/skills/objectstack-data/rules/security.md b/skills/objectstack-data/rules/security.md index 5f3085c60a2..1e4ca33b44c 100644 --- a/skills/objectstack-data/rules/security.md +++ b/skills/objectstack-data/rules/security.md @@ -124,8 +124,8 @@ A set's `fields` entry (`'OBJECT.FIELD': { readable, editable }`) may carry - Locked for **everyone** at that stage → a [validation rule](./validation.md) on the object (`readonlyWhen` likewise strips the write for everyone). -- Locked for **some roles** only → `when` on the field entries of each set - that loses the edit; the other sets keep their grant. +- Locked for **some permission sets** only → `when` on the field entries of + each set that loses the edit; the other sets keep their grant. ```typescript // finance may not change the contract's legal terms after approval @@ -137,7 +137,7 @@ fields: { - **Edit vote only.** Reads are row-blind; `readable: false` beside `when` is refused at parse time. - **Same object.** Fields of the entry's object vs a literal or `current_user.*`; - no relation reach (refused at `os validate` / publish, never dropped). + no relation reach (refused at `os validate` / publish). - **Pre-image.** An update is judged on the stored row, so a write cannot unlock itself by moving `status` back; an insert on the row as sent. - **Locks, never grants.** On unmatched rows the entry is absent; another held