Skip to content

docs(skills): a system flow is refused identity-assignment writes in objectstack-automation - #26566

Merged
os-zhuang merged 3 commits into
mainfrom
claude/project-thread-h51588
Oct 27, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/project-thread-h51588

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #26502

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:objectstack-automation 技能在「Elevate the write, not the flow」旁加一段,说明 runAs: 'system' 流程写四张身份分配表会被 PERMISSION_DENIED 拒绝,并给出拒绝文案自带的补救;新增 eval 11 作为 eval 1 的反例;两行 token 天花板按裁决 A 抬到实测值。
  • 为什么改:技能把 runAs: 'system' 叫作 trusted-writer channel,AI 照着写系统流程去授予权限集,运行时被拒,技能却从未提过,作者拿不到补救路径。
  • 风险与回滚:只动发布技能的文字、一条 eval、两行棘轮和一个 @objectstack/skills patch changeset;无运行时代码。回滚 = revert 本 PR。
  • 先看:skills/objectstack-automation/SKILL.md 新段落(措辞是否与运行时拒绝一致);evals/flows-triggers-approvals.json 的 eval 11;scripts/check-skills-token-ratchet.mjs 两行上限及注释。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期末行 ✓ check-skills-token-ratchet: 81 authored bundle file(s) within their ceilings。
  • 需要你做的:在这个 draft 上由审批账号给一次 APPROVED(Tier H,skills/**)。

What changes

skills/objectstack-automation/SKILL.md: one paragraph placed right after 「Elevate the write, not the flow」, inside the same quote block:

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).

The remedy is the refusal's own prescription, not a third wording. The flow source of plugin-security/src/flow-system-identity-write-refusal.ts prescribes "Set the flow's runAs to 'user': the delegated-administration gate then judges the user the run acts for, and records that user as the grant's granted_by." The membership half is MEMBERSHIP_PRESCRIPTION in packages/spec/src/identity/identity-assignment-body-writes.ts. The sentence about the subflow closes the gap with the paragraph right above it, which tells the author to move an elevated write into a dedicated system subflow. The paragraph names the refusal and the door that judges the write; it describes no way around it. It mirrors the wording PR #26501 uses in hooks.mdx and actions.mdx and does not touch content/docs/**.

skills/objectstack-automation/evals/flows-triggers-approvals.json: eval 11, the counter-case to eval 1's readonly sweep. Inserted as text, so ids 1 to 10 are byte-identical.

  • 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."
  • must_contain: runAs: 'user', sys_user_permission_set, PERMISSION_DENIED, delegated-administration, granted_by. must_not_contain: runAs: 'system', sudo. A script checked that the expected output carries every must_contain string and neither must_not_contain string.

scripts/check-skills-token-ratchet.mjs: the two rows of the files above, raised under ruling A. Each row comment carries its arithmetic and the tap.

.changeset/26502-automation-skill-identity-assignment-refusal.md: a patch changeset for @objectstack/skills, with both declaration lines. skills/** publishes: the package's build (packages/skills/scripts/sync-catalog.mjs) copies the catalog into its dist, which its files list ships. So this PR carries a changeset and no skip-changeset label.

Duplicate scan (ruling A, first step)

  • SKILL.md: read in full. The runAs guidance sits at L164 to L201 (the readonly strip, elevate the write, pin the organization, requiredPermissions, hook runAs). No sentence in the file restates another, and none names the four objects or the refusal before this PR. Nothing to cut, 0 bytes.
  • evals/flows-triggers-approvals.json: the ten existing evals share no sentence. Eval 1 teaches runAs: 'system' for a readonly sweep; eval 11 is its counter-case, not a restatement. Nothing to cut, 0 bytes.

Ratchet arithmetic

Ruling: the maintainer's tap on the seat's decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, 2026-10-27 01:21Z, verbatim and untranslated: 「A:先削重复,余额按实测提高」. Option A in full: 「八张卡的 PR 各自削重复后,把所改文件的天花板行提高恰好差额,正文引用你这句;教学和 eval 都落。」

File Before (bytes) After (bytes) Ceiling before Ceiling after
SKILL.md (priced, the 20-byte stamp line excluded) 24654 25429 6164 6358
evals/flows-triggers-approvals.json 9705 10893 2427 2724

Both rows had 0 tokens of headroom. Each ceiling is ceil(bytes / 4) of the new file, pinned AT the measurement.

Size readings:

Reading Before After
SKILL.md lines 441 452
eval file lines 105 124
Whole package, SKILL.md + references/**/*.md, lines 1013 1024
Whole package, same set, bytes 56948 57723

Tests

No runtime code changes, so no package tests apply. The skill's own checks ran on caee07d4 (the card commit 6609364d plus a merge of origin/main at ab3c776a); the ratchet gate ran again on head 4b5b0436, which adds only the changeset:

Card-named checks
  • node scripts/check-skills-token-ratchet.mjs: exit 0, ✓ check-skills-token-ratchet: 81 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted.
  • node scripts/check-skills-token-ratchet.mjs --self-test: exit 0, 79 cases pass.
  • pnpm check:role-word: exit 0. Neither file contains the word.
  • pnpm check:skill-frame-sync, pnpm check:skill-compatibility, pnpm check:skill-top-level-keys, pnpm check:pm-skill-ratchet: exit 0 each.
  • pnpm --filter @objectstack/spec check:skill-examples: exit 0, 265 prose examples type-check across 3 surface(s).
  • pnpm --filter @objectstack/spec run check:skill-docs: exit 0.

Acceptance notes

  • "The skill's guidance and the runtime agree": the paragraph states the refusal that flow-system-identity-write-refusal.ts applies, with the same six verbs, the four objects and the code. It gives the remedy the refusal prescribes for a flow, and the membership remedy from identity-assignment-body-writes.ts.
  • "The skill's eval/compat checks green": see Tests.
  • The new paragraph covers a flow only. Hooks and action bodies are refused the same way; their wording lives in hooks.mdx and actions.mdx through PR docs: a system-elevated hook or action body is refused identity-assignment writes #26501, and this skill's hook paragraph points to objectstack-data. Noted, not filed.

Verification

Derived gate run on 4b5b043

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 52 families on caee07d4 and 63 on 4b5b0436; the 11 the changeset added ran on 4b5b0436. 62 ran locally with exit 0. pnpm check:dual-build-cjs-loads is NOT MEASURED: it needs the whole tree built, and CI's Build Core runs it. --ran reconciliation: ✓ dispatch-gates --ran: 63 derived famil(ies) accounted for — 62 run, 1 NOT-MEASURED. pnpm check:pm-dispatch-gates ran detached; its verdict line reads ✓ dispatch-gates self-test (fast tier): 1935 cases pass. The build closure for the client, client-react, lint and formula packages ran under the verify lock with VERDICT command-exit 0.

Tier H: skills/** is a governed surface. This PR stays a draft until an authorized approver reviews it.


Generated by Claude Code

…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>
…ation 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 <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4b5b0436fe98dba3ce7877df6f34c9ea47f8648c
Local-runs: none
Scope: skills/objectstack-automation/SKILL.md, skills/objectstack-automation/evals/flows-triggers-approvals.json + scripts/check-skills-token-ratchet.mjs, packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts, packages/spec/src/identity/identity-assignment-body-writes.ts, content/docs/permissions/system-context.mdx

Director seat (summon #52, ledger objectstack#12708), after the fact by the maintainer's word 「#26566、#26568、#26570、#26585 也要追加复审,虽然我已经点了合并」: the PR was approved by os-zhuang at 4b5b0436 (2026-10-27T09:51Z) and merged 2026-10-27T10:05Z; this record reads the same head, read-only — the diff, card #26502, the cited code on main, the head's check runs. dispatch-gates --tier on the PR's paths: skills/** is a governed path ⇒ the rules face, CONTRACT_REVIEW_TIER; served at CONTRACT_REVIEW_TIER.

① Derived judgments

  • The new teaching matches the runtime it describes. The SKILL.md paragraph ("Elevation stops at identity assignment": a runAs: 'system' flow's insert/update/delete/transfer/restore/purge of sys_user_position, sys_user_permission_set, sys_invitation, sys_member is refused with PERMISSION_DENIED; positions and permission sets are granted from a runAs: 'user' flow; invitations and memberships move through the organization endpoints, row 1b) is read against flow-system-identity-write-refusal.ts (the refusal, its object list and error code) and the spec's MEMBERSHIP_PRESCRIPTION in identity-assignment-body-writes.ts (the prescription the skill restates) and content/docs/permissions/system-context.mdx row 1b. Object names, the six operations, the error code and the two prescribed routes agree in all four places; no sentence in the skill promises something the refusal does not do.
  • Eval 11 is derived from the same contract. must_contain names runAs: 'user', sys_user_permission_set, PERMISSION_DENIED, delegated-administration, granted_by — every one is a string the new paragraph or the delegated-administration section already carries; must_not_contain [runAs: 'system', sudo] refuses the two wrong answers the card names. The fixture asks the question the card's user asked (a system flow granting a permission set); consistent.
  • Token ratchet moved by ruling, not by hand. check-skills-token-ratchet.mjs ceilings 6164 → 6358 (SKILL.md) and 2427 → 2724 (evals json) carry the arithmetic in the comment (bytes before/after, ceil(n/4), headroom 0) and quote the maintainer's tap on the decision card verbatim: 「A:先削重复,余额按实测提高」. Pinned AT the measurement, headroom 0 — the next sentence added to either file moves the ceiling again, which is what the ruling's "按实测" asks for. Class ③ (maintainer-directed) holds.

② Semver level

@objectstack/skills takes a patch changeset (.changeset/26502-automation-skill-identity-assignment-refusal.md); Clause-②: no, Metadata-protocol: none in the PR body and the changeset, equal. The skill catalog ships in the package's dist (the body names sync-catalog.mjs and the files list), so a changeset is owed and present; teaching text and one eval fixture publish no new accept set — patch is right under AGENTS.md Post-Task Checklist step 3.

③ Boundary flags

  • Governed path (skills/**): landed under a GOVERNED_APPROVERS approval (os-zhuang, at this head) — the seat records, it does not approve.
  • Check runs pinned to 4b5b0436: 32 success, 11 expected skips, 0 other — Lint & Repo Gates, Lint · scoped · tooling self-tests, Governed Surface Queue Guard, Check Changeset, Test Core (3 shards), TypeScript Type Check, the three claim guards. No local re-run.
  • The eval is a fixture, not a run: nothing on this head executes eval 11 against a model, so "the skill now answers correctly" is asserted by the strings, not measured. Expected for evals/*.json; noted so nobody reads the green as an eval pass.

④ Design judgment

  • SKILL.md: the paragraph holds against its cited code and the card's scenario (a flow author who reaches for runAs: 'system' to assign a permission set is told the refusal, the code, the route). It restates the spec's prescription in the skill's voice without adding a rule the runtime does not enforce; the "row 1b" link gives the reader the authoritative table rather than a second copy. Correct placement, no restatement elsewhere in the file (the ratchet comment's "0 bytes to cut" matches my read).
  • evals/flows-triggers-approvals.json: eval 11 pins the contract's load-bearing strings and refuses the two escalation answers; it does not pin prose that could legitimately be rephrased. Holds.

Implemented-by: claude/project-thread-h51588
Reviewed-by: session_01ADHkmQr732toX135tyo5zs

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Retracted: this comment was a duplicate post of the director seat's contract-review record on this PR; the record of record is comment 6439727886 above (same head, same verdict). Nothing else changes.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

3 participants