Skip to content

Commit 6609364

Browse files
committed
docs(skills): a system flow is refused identity-assignment writes in objectstack-automation
The runAs 'system' guidance called system context the trusted-writer channel and never said that a system flow's governed writes to sys_user_position, sys_user_permission_set, sys_invitation and sys_member are refused PERMISSION_DENIED. Add one paragraph beside "Elevate the write, not the flow" stating the refusal and the remedy the refusal prescribes, linking system-context row 1b, and eval 11 as the counter-case to eval 1. Raise the two touched token-ratchet rows by the measured remainder under the maintainer's ruling A. Claude-Session: https://claude.ai/code/session_01JQwxba5RipjC9aRL8Cjjux Co-authored-by: Claude <noreply@anthropic.com>
1 parent d2ca707 commit 6609364

3 files changed

Lines changed: 54 additions & 2 deletions

File tree

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,20 @@ export const CEILINGS = new Map([
407407
// measurement. Maintainer ruling 2026-10-27 on the seat's decision card
408408
// 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim and
409409
// untranslated: 「A:先削重复,余额按实测提高」.
410-
['skills/objectstack-automation/SKILL.md', 6164],
410+
// 24654 -> 25429 (#26502): one paragraph beside "Elevate the write, not the
411+
// flow" — a `runAs: 'system'` flow's insert, update, delete, transfer, restore
412+
// or purge of the four identity-assignment objects is refused
413+
// `PERMISSION_DENIED`, a dedicated system subflow included, and the refusal's
414+
// own remedy (the grant from a `runAs: 'user'` flow, invitations and
415+
// memberships through the organization endpoints), linking system-context
416+
// row 1b. Paid down first: the file's `runAs` guidance restates nothing
417+
// elsewhere in it, nothing to cut (0 bytes). Arithmetic in priced bytes
418+
// (stamp line excluded): 24654 -> 25429 (+775) -> ceil(25429 / 4) = 6358;
419+
// headroom was 2 bytes, 0 tokens, so the ceiling moves +194, pinned AT
420+
// the measurement. Maintainer ruling 2026-10-27 on the seat's decision card
421+
// 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim and
422+
// untranslated: 「A:先削重复,余额按实测提高」.
423+
['skills/objectstack-automation/SKILL.md', 6358],
411424
// 13783 -> 13892 (2026-08-31 app-repo-principles raise, see the block above).
412425
// The invariant-vs-transition-gate choice, placed in the field-conditional-rule
413426
// bullet list where the tool is actually picked. Both wrong picks are silent:
@@ -807,7 +820,16 @@ export const CEILINGS = new Map([
807820
// ceiling moves +286, pinned AT the measurement. Same maintainer ruling as
808821
// the SKILL.md row above (2026-10-27, verbatim and untranslated):
809822
// 「A:先削重复,余额按实测提高」.
810-
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2427],
823+
// 9705 -> 10893 (#26502): eval 11, the counter-case to eval 1's readonly sweep
824+
// — an author asks a system flow to grant a permission set; the ideal answer
825+
// names the `PERMISSION_DENIED` refusal and routes the grant through a
826+
// `runAs: 'user'` flow, written so it never spells either `must_not_contain`
827+
// string. Paid down first: the file's ten fixtures share no sentence,
828+
// nothing to cut (0 bytes). 9705 -> 10893 bytes (+1188) -> ceil(10893 / 4)
829+
// = 2724; headroom was 3 bytes, 0 tokens, so the ceiling moves +297, pinned
830+
// AT the measurement. Same maintainer ruling as the SKILL.md row above
831+
// (2026-10-27, tap A verbatim and untranslated): 「A:先削重复,余额按实测提高」.
832+
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2724],
811833
// 894 -> 1095 (#26179): one regression case for the field permission `when`
812834
// (the contract prompt: lock the legal terms for finance from approval on,
813835
// legal keeps the edit) — expected: `when` on the finance set's entries with

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,17 @@ variables: [
179179
> dedicated `runAs: 'system'` flow and call it from a `subflow` node — raising
180180
> the whole flow silently elevates every other write in it.
181181
>
182+
> **Elevation stops at identity assignment.** A `runAs: 'system'` flow's
183+
> insert, update, delete, transfer, restore or purge of `sys_user_position`,
184+
> `sys_user_permission_set`, `sys_invitation` or `sys_member` is refused with
185+
> `PERMISSION_DENIED`, and moving that step into a dedicated `runAs: 'system'`
186+
> subflow does not lift it: a governed identity write is judged by a door that
187+
> judges a person. Grant positions and permission sets from a flow whose
188+
> `runAs` is `'user'` — the delegated-administration gate then judges the user
189+
> the run acts for and records them as the grant's `granted_by`. Invitations
190+
> and memberships change through the organization endpoints (row 1b,
191+
> https://objectstack.ai/docs/permissions/system-context#1-authorization-and-scoping).
192+
>
182193
> **A `runAs: 'system'` sweep must pin its organization.** System context has no
183194
> trigger user, so nothing narrows the query: a scan or rollup with no
184195
> organization predicate reads and writes across every tenant. The tenant column

‎skills/objectstack-automation/evals/flows-triggers-approvals.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@
100100
"must_contain": ["getRecordLock", "locked"],
101101
"must_not_contain": ["lock_record", "approvalsVisibleToReaders: true"]
102102
}
103+
},
104+
{
105+
"id": 11,
106+
"prompt": "When a `contractor` record is marked `onboarded`, grant its linked user (`contractor.user_id`) the `contractor_portal` permission set. Make the flow run as system so the grant always lands.",
107+
"expected_output": "Not as system: a system-context insert into `sys_user_permission_set` (as into `sys_user_position`, `sys_invitation` or `sys_member`) is refused with `PERMISSION_DENIED` — elevation does not lift a governed identity write, a dedicated system subflow included. Emits the `record_change` flow with `runAs: 'user'`: its `create_record` on `sys_user_permission_set` is judged by the delegated-administration gate as the user whose write fired the flow, recorded as the grant's `granted_by`; a user the gate would not let grant the set is refused, which is the rule, not a defect to route around.",
108+
"files": [],
109+
"assertions": {
110+
"must_contain": [
111+
"runAs: 'user'",
112+
"sys_user_permission_set",
113+
"PERMISSION_DENIED",
114+
"delegated-administration",
115+
"granted_by"
116+
],
117+
"must_not_contain": [
118+
"runAs: 'system'",
119+
"sudo"
120+
]
121+
}
103122
}
104123
]
105124
}

0 commit comments

Comments
 (0)