From 6609364d244895476dd041e2d18e80cd16a8bd4a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 08:18:19 +0000 Subject: [PATCH 1/2] 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 --- scripts/check-skills-token-ratchet.mjs | 26 +++++++++++++++++-- skills/objectstack-automation/SKILL.md | 11 ++++++++ .../evals/flows-triggers-approvals.json | 19 ++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index d7530d69d18..22e6424f4c8 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -407,7 +407,20 @@ export const CEILINGS = new Map([ // measurement. Maintainer ruling 2026-10-27 on the seat's decision card // 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim and // untranslated: 「A:先削重复,余额按实测提高」. - ['skills/objectstack-automation/SKILL.md', 6164], + // 24654 -> 25429 (#26502): one paragraph beside "Elevate the write, not the + // flow" — a `runAs: 'system'` flow's insert, update, delete, transfer, restore + // or purge of the four identity-assignment objects is refused + // `PERMISSION_DENIED`, a dedicated system subflow included, and the refusal's + // own remedy (the grant from a `runAs: 'user'` flow, invitations and + // memberships through the organization endpoints), linking system-context + // row 1b. Paid down first: the file's `runAs` guidance restates nothing + // elsewhere in it, nothing to cut (0 bytes). Arithmetic in priced bytes + // (stamp line excluded): 24654 -> 25429 (+775) -> ceil(25429 / 4) = 6358; + // headroom was 2 bytes, 0 tokens, so the ceiling moves +194, pinned AT + // the measurement. Maintainer ruling 2026-10-27 on the seat's decision card + // 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim and + // untranslated: 「A:先削重复,余额按实测提高」. + ['skills/objectstack-automation/SKILL.md', 6358], // 13783 -> 13892 (2026-08-31 app-repo-principles raise, see the block above). // The invariant-vs-transition-gate choice, placed in the field-conditional-rule // bullet list where the tool is actually picked. Both wrong picks are silent: @@ -807,7 +820,16 @@ export const CEILINGS = new Map([ // ceiling moves +286, pinned AT the measurement. Same maintainer ruling as // the SKILL.md row above (2026-10-27, verbatim and untranslated): // 「A:先削重复,余额按实测提高」. - ['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2427], + // 9705 -> 10893 (#26502): eval 11, the counter-case to eval 1's readonly sweep + // — an author asks a system flow to grant a permission set; the ideal answer + // names the `PERMISSION_DENIED` refusal and routes the grant through a + // `runAs: 'user'` flow, written so it never spells either `must_not_contain` + // string. Paid down first: the file's ten fixtures share no sentence, + // nothing to cut (0 bytes). 9705 -> 10893 bytes (+1188) -> ceil(10893 / 4) + // = 2724; headroom was 3 bytes, 0 tokens, so the ceiling moves +297, pinned + // AT the measurement. Same maintainer ruling as the SKILL.md row above + // (2026-10-27, tap A verbatim and untranslated): 「A:先削重复,余额按实测提高」. + ['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2724], // 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 diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 80341938b50..3ebc073087e 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -179,6 +179,17 @@ variables: [ > dedicated `runAs: 'system'` flow and call it from a `subflow` node — raising > the whole flow silently elevates every other write in it. > +> **Elevation stops at identity assignment.** A `runAs: 'system'` flow's +> insert, update, delete, transfer, restore or purge of `sys_user_position`, +> `sys_user_permission_set`, `sys_invitation` or `sys_member` is refused with +> `PERMISSION_DENIED`, and moving that step into a dedicated `runAs: 'system'` +> subflow does not lift it: a governed identity write is judged by a door that +> judges a person. Grant positions and permission sets from a flow whose +> `runAs` is `'user'` — the delegated-administration gate then judges the user +> the run acts for and records them as the grant's `granted_by`. Invitations +> and memberships change through the organization endpoints (row 1b, +> https://objectstack.ai/docs/permissions/system-context#1-authorization-and-scoping). +> > **A `runAs: 'system'` sweep must pin its organization.** System context has no > trigger user, so nothing narrows the query: a scan or rollup with no > organization predicate reads and writes across every tenant. The tenant column diff --git a/skills/objectstack-automation/evals/flows-triggers-approvals.json b/skills/objectstack-automation/evals/flows-triggers-approvals.json index 999b39b6cd8..793f4beaa3f 100644 --- a/skills/objectstack-automation/evals/flows-triggers-approvals.json +++ b/skills/objectstack-automation/evals/flows-triggers-approvals.json @@ -100,6 +100,25 @@ "must_contain": ["getRecordLock", "locked"], "must_not_contain": ["lock_record", "approvalsVisibleToReaders: true"] } + }, + { + "id": 11, + "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.", + "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.", + "files": [], + "assertions": { + "must_contain": [ + "runAs: 'user'", + "sys_user_permission_set", + "PERMISSION_DENIED", + "delegated-administration", + "granted_by" + ], + "must_not_contain": [ + "runAs: 'system'", + "sudo" + ] + } } ] } From 4b5b0436fe98dba3ce7877df6f34c9ea47f8648c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 08:36:12 +0000 Subject: [PATCH 2/2] chore(changeset): patch @objectstack/skills for the objectstack-automation identity-assignment paragraph skills/** ships in @objectstack/skills (sync-catalog copies it into the package's dist), so the teaching change publishes and carries a patch changeset with both declaration lines. Claude-Session: https://claude.ai/code/session_01JQwxba5RipjC9aRL8Cjjux Co-authored-by: Claude --- ...2-automation-skill-identity-assignment-refusal.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/26502-automation-skill-identity-assignment-refusal.md diff --git a/.changeset/26502-automation-skill-identity-assignment-refusal.md b/.changeset/26502-automation-skill-identity-assignment-refusal.md new file mode 100644 index 00000000000..0086cac9c55 --- /dev/null +++ b/.changeset/26502-automation-skill-identity-assignment-refusal.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): objectstack-automation says a `runAs: 'system'` flow is refused identity-assignment writes, and names the remedy + +Clause-②: no +Metadata-protocol: none + +- **Before.** The `runAs: 'system'` guidance called system context the trusted-writer channel and never said that a system flow's insert, update, delete, transfer, restore or purge of `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member` is refused with `PERMISSION_DENIED`. An agent following the skill wrote a system flow to grant a permission set and met the refusal at run time. +- **Now.** A paragraph beside "Elevate the write, not the flow" states the refusal, says a dedicated system subflow does not lift it, and gives the remedy the refusal prescribes: grant positions and permission sets from a flow whose `runAs` is `'user'`, judged by the delegated-administration gate as the user it acts for; change invitations and memberships through the organization endpoints. Eval 11 in `evals/flows-triggers-approvals.json` checks that an answer routes such a grant through a `runAs: 'user'` flow. +- No schema, key or behaviour changes.