Repository navigation
Commit 07586f5
feat(spec)!: refuse at save a system hook or script action body that writes an identity-assignment object (#26430)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBRDFqiYQLscu1ybuLZGACQn)_
Fixes #25653
Clause-②: no (narrowing)
Metadata-protocol: narrowed
## 维护者速读
- 改了什么:`HookSchema` 与 `ActionSchema` 在保存时拒收「以 system 提权运行、且沙箱 JS
正文写四个身份分配对象(岗位持有、权限集授予、邀请、成员)」的钩子/动作;`Hook.runAs` 说明补上「提权不触及身份分配」;新增一条
ADR-0087 D3 条目。
- 为什么改:运行时早已拒绝这类写入(上两片已合入),但保存门仍然接受,作者只能在运行失败时才发现;裁决 6384657105 字母 A
要求整个家族在保存时同样拒收。
- 风险与回滚:只收窄 spec 的接受集,运行时不变;仓内普查为零命中,不会有已发布的钩子或动作开始被拒;回滚即撤回本 PR。
-
先看:`packages/spec/src/identity/identity-assignment-body-writes.ts`(判定与拒收文案)、`packages/spec/src/data/hook.zod.ts`
的
`refuseElevatedIdentityAssignmentBodyWrite`、`packages/spec/src/ui/action.zod.ts`
的 `refuseIdentityAssignmentBodyWrite`。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
src/identity/identity-assignment-body-writes.test.ts`,预期 28 passed。
- 需要你做的:无,席位落地。
## What changes
App-authored automation does not use elevation to write
`sys_user_position`, `sys_user_permission_set`, `sys_invitation` or
`sys_member` (ruling record 6384657105, letter A). The run-time half
landed in #25954 and #25982: the data middleware refuses that write with
`PERMISSION_DENIED`. This PR is the save-time half that triage routed to
`domain:spec` (6431012701).
- **New leaf
`packages/spec/src/identity/identity-assignment-body-writes.ts`**
(import-free, not exported from the barrel). It holds the four-object
set, the reading of a sandboxed body's source (a write method `insert`,
`update`, `delete`, `updateMany`, `deleteMany` or `upsert` on
`ctx.api.object('OBJECT')`, called on the handle or on a name the body
bound it to), and the refusal text with its prescription per source.
- **`HookSchema`** gains a second object-level check,
`refuseElevatedIdentityAssignmentBodyWrite`. It refuses a hook that
declares `runAs: 'system'` and carries a `language: 'js'` body whose
source writes one of the four objects. There is one `custom` issue per
object, at `body.source`. The JSON-stage hook in `stack.zod.ts` keeps
the check through `safeExtend`.
- **`ActionSchema`** gains `refuseIdentityAssignmentBodyWrite`: the same
check for a `type: 'script'` action, whose body always runs as system.
- **`Hook.runAs`**: the JSDoc and the `.describe()` now say that
elevation does not reach identity assignment, as `FlowSchema.runAs`
does. The hook reference page was regenerated (`gen:docs`).
- **ADR-0087 D3 entry**
`hook-action-body-system-identity-assignment-write-refused` sits beside
`18.flow-run-as-system-identity-assignment-write-refused`. It adds no
tombstone and no D2 conversion.
- **Changeset**: `@objectstack/spec` minor, BREAKING, `adr-0087:
registered`.
## Tests
`packages/spec/src/identity/identity-assignment-body-writes.test.ts`, 28
cases:
- The reader names each of the four objects for every write method and
every quote form. It also names a write on a bound handle, and each
object once.
- `HookSchema`, the registered `hook` type schema, `ActionSchema` and
the registered `action` type schema each refuse each object at
`body.source`, with the right prescription.
- `defineStack` refuses with `422 STACK_SCHEMA_INVALID` at
`hooks.N.body.source`. It refuses the action too. An artifact's parse
refuses the hook.
- The D3 entry is registered at 18 with no conversion and no tombstone.
- Controls (what the run-time refusal leaves legal too): reads of the
four objects, writes to other objects, a hook under `runAs: 'user'`, an
expression body and a code handler.
## Acceptance notes
- Census at `origin/main` `e931621a`: no hook or action body in this
repository calls `ctx.api.object(...)` on one of the four objects (`git
grep`, 0 hits; the control pattern over every `sys_*` object hits 6
files). So no shipped body starts being refused. hotcrm was not
re-measured from this session. The run-time slices' census found none
there.
- The four-object set and the prescription text now live in two places:
this spec leaf and plugin-security's
`FLOW_SYSTEM_IDENTITY_WRITE_OBJECTS`. The claim's file surface is
spec-only, so plugin-security does not import the spec set yet. That
would also make the leaf a public export.
- `FlowSchema` has no save-time twin of this check. A `runAs: 'system'`
flow's write node aimed at one of the four objects is refused only at
run time, by the earlier change. This is noted here, not filed.
## Verification
All readings below are at head `1234236ab` unless noted.
<details><summary>Spec package</summary>
- `pnpm --filter @objectstack/spec test` under `os-verify-lock`: VERDICT
command-exit 0 · Test Files 728 passed (728) · Tests 22037 passed | 1
todo. The pre-merge head `2a0debc6` read 727 files / 22012 passed.
- `pnpm --filter @objectstack/spec typecheck`: exit 0
(`check:test-typecheck` OK).
- `eslint --no-inline-config` on the 5 changed `.ts` files: exit 0.
- `pnpm --filter @objectstack/spec check:generated`: all 14 artifacts up
to date after the merge of `origin/main`. The only regenerated artifact
is `content/docs/references/data/hook.mdx`.
</details>
<details><summary>Ablation (one behaviour: the body-source
reader)</summary>
`node scripts/ablation-replace.mjs` replaced `return [...named];` with
`return [];` in the leaf. The anchor went 1 → 0 and the blob changed.
With the mutation, the test file read 20 failed | 8 passed. The 20 were
exactly the refusal rows. The 8 that stayed green were the 6 controls
and the 2 ledger pins. On restore, the blob equalled HEAD and `git diff
HEAD` was empty. The spec suite imports from `src/`, so no rebuild was
involved.
</details>
<details><summary>Gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on head `1234236ab`: 114 families. All 114
exited 0 on the first pass.
- `dispatch-gates --ran` with every exit code recorded: 114 derived, 114
run, 0 NOT-MEASURED, 0 UNRUN.
- On the pre-merge head `2a0debc6` the same derivation (114 families,
two different) also ended 114/114 exit 0. Eight of them first answered
exit 3 (a prerequisite: an unbuilt workspace or the shallow clone), and
they exited 0 after a full build and fetching the pinned fixture commit.
- `node scripts/check-dev-prereqs.mjs` (a local-only family): exit 0.
</details>
<details><summary>Dogfood (full battery, final head)</summary>
`pnpm --filter @objectstack/dogfood test` under `os-verify-lock` at
`1234236ab`, after a full workspace build (118/118): VERDICT
command-exit 1 · Test Files 2 failed | 298 passed | 3 skipped (303) ·
Tests 10 failed | 2522 passed | 18 skipped (2550).
The 10 failures are all in `test/authz-conformance.test.ts` (the #17111
docblock-figure pins: "docblock says 80 rows, the table holds 81" for
`packages/rest/src/rest-route-ledger.ts`) and
`test/authz-probe-blind-spot.test.ts` (the `this.routeManager.register(`
control in `packages/rest/src/rest-server.ts` reads 63, pinned at 62).
They are not caused by this diff. It touches no file under
`packages/rest` or `packages/qa`: `git diff e677770 1234236 --
packages/rest packages/qa` is empty. The row came from `6a2866af7`
(#26361), which is already an ancestor of this branch's base `e931621a`
(`git merge-base --is-ancestor` exit 0). So `main`'s own full battery
carries the same reds.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (eslint on the changed
files locally; the repo-wide run is CI's).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzecHS6dL9h6WLdsGCH
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzecHS6dL9h6WLdsGCH)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f9e871a commit 07586f5
7 files changed
Lines changed: 526 additions & 6 deletions
File tree
- .changeset
- content/docs/references/data
- packages/spec/src
- data
- identity
- migrations/entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
17 | 24 | | |
18 | 25 | | |
19 | 26 | | |
| |||
134 | 141 | | |
135 | 142 | | |
136 | 143 | | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
137 | 190 | | |
138 | 191 | | |
139 | 192 | | |
| |||
462 | 515 | | |
463 | 516 | | |
464 | 517 | | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
465 | 529 | | |
466 | 530 | | |
467 | 531 | | |
| |||
484 | 548 | | |
485 | 549 | | |
486 | 550 | | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
487 | 557 | | |
488 | 558 | | |
489 | 559 | | |
| |||
494 | 564 | | |
495 | 565 | | |
496 | 566 | | |
497 | | - | |
498 | | - | |
499 | | - | |
500 | | - | |
501 | | - | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
502 | 573 | | |
503 | 574 | | |
504 | 575 | | |
| |||
0 commit comments