Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> 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. +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', 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
Expand Down Expand Up @@ -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],
Expand Down
3 changes: 2 additions & 1 deletion skills/objectstack-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
]
}
29 changes: 29 additions & 0 deletions skills/objectstack-data/rules/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
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).
- **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:
Expand Down
Loading