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
12 changes: 12 additions & 0 deletions .changeset/26502-automation-skill-identity-assignment-refusal.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 24 additions & 2 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
]
}
Loading