Skip to content

Commit ffcb44d

Browse files
committed
Merge origin/main into claude/project-thread-wq49tp
Conflict on scripts/pm/measurement-claim-triage.mjs (modified on main by the sdui lockstep retirement, deleted here) resolved by keeping the deletion. Claude-Session: https://claude.ai/code/session_01LbdJFR3AmcKLZKQafYE1tN Co-authored-by: Claude <noreply@anthropic.com>
2 parents 77e4b93 + 2137155 commit ffcb44d

120 files changed

Lines changed: 4770 additions & 5894 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
'@objectstack/core': patch
4+
---
5+
6+
fix(plugin-security)!: a permission set that shares a held position's name is no longer granted; declare it in the position's `permissionSets` (#24757)
7+
8+
Clause-②: yes (narrowing: a holder of position `P` is no longer granted the permission set named `P` unless `P`'s definition names it in `permissionSets`; widened: `@objectstack/core`'s `UserAuthzGrants` and `ResolvedAuthzContext` gain the additive field `directPermissions`)
9+
Metadata-protocol: none
10+
Ruling-ref: #13419 ruling B (`5472608110`); 「甲」 as amended (`6303865025`); order and unlock on #24757 (`6290781571`, `6392096931`); #25799 seat ruling A (claims `6414302166`, `6414315157`)
11+
12+
ADR-0131 D3/D4: a position grants only the permission sets its definition's `permissionSets` names — the binding is written, not inferred. The security plugin's set resolution still requested every held position's NAME as a permission-set name too (the position-name fold), so a set that happened to share a position's name was granted to its holders without anyone writing the binding. #13419 ruling B retires that channel; HotCRM declared its bindings first (objectstack-ai/hotcrm#2200, released in hotcrm `v6.0.0`).
13+
14+
What changes:
15+
16+
- `resolvePermissionSetsForContext` requests only the context's `permissions` and the baseline. A held position's name reaches no set. `dropDeactivatedFoldedSets`, the second activation read that only the fold needed, is gone.
17+
- The `position_name_fold_grant` warning keeps its event name and fields and now says the opposite: the same-named set is **not** granted, and how to grant it (`permissionSets: ['P']`) or to rename one of the two. Its payload carries `granted: false`. It is still logged once per position name per process.
18+
- `describePositionGrants` lists only the sets a position's definition names, so it no longer serves `via: 'name'`. The `'name'` member of `PositionGrantVia` (`@objectstack/spec`) and its UI badge are retired separately in #26022; until then the value is published but never served.
19+
- The delegated-administration gate judges an assignment of position `P` on the sets `P`'s definition names only. A set that only shares `P`'s name is no longer walked for the allowlist, the containment check or the organization boundary, because no assignment grants it.
20+
- `explainAccess` attributes a set to `position:P` when `P`'s definition names it, and says `direct grant` otherwise. When the position definitions cannot be read it says so, rather than guessing either. A set held both through the user's own grant row and through a held position's definition reads `direct grant + position:P`, so revoking the position is not mistaken for removing the access (#25799).
21+
- `@objectstack/core`: `UserAuthzGrants` and `ResolvedAuthzContext` gain `directPermissions: string[]`, the names conferred by the user's own `sys_user_permission_set` rows that apply in the active organization (a set a position definition also names included, a set only a definition names excluded). It is additive and grants nothing; the explainer reads it. Code that builds either object by hand must now set it (`directPermissions: []` when there are no grant rows).
22+
- The lint gate `check-position-name-fold-loaders` and the census script `measure-position-name-fold-census` are deleted. They guarded the fold's only route, which no longer exists.
23+
24+
**BREAKING** for an environment that relied on a shared name to grant a set.
25+
26+
- **Before.** A user holding position `sales_manager` was granted the permission set `sales_manager` whether or not the position's definition named it.
27+
- **Now.** That user is granted only the sets the `sales_manager` definition lists in `permissionSets`. The first resolution that would have folded the set logs `position_name_fold_grant` with `granted: false`, naming the position and the fix.
28+
- **Fix.** Name the set in the position: `permissionSets: ['sales_manager']`. If the shared name is a coincidence, nothing needs to change.
29+
- **Cloud ordering.** An environment running HotCRM must be on the HotCRM release that declares its bindings (`v6.0.0` or later) before it takes this release.
30+
31+
<!-- adr-0087: not-required (no-migration-prescription) a run-time narrowing in the security plugin's permission-set resolution: a held position's name is no longer requested as a permission-set name. No authorable key, spelling or stored shape moves, the position and permission-set schemas parse exactly as before, and no stored row is read, rewritten or converted. The repair is the author's choice between naming the set in the position's `permissionSets` and leaving it ungranted, which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this verdict (not already-registered); nothing exported from `@objectstack/spec` moves in this change; and the one type change, the additive `directPermissions` on `@objectstack/core`'s resolver output, is a per-request value no stored artifact or authored metadata carries, so no conversion has a source to rewrite. -->

‎.changeset/25610-notify-email-field-recipient.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ A `notify` node's `recipients` takes a fourth form, as the whole value: `{ email
2222
- The form is the whole `recipients` value, never a list entry.
2323
- `@objectstack/lint` refuses, at `os validate`, `os lint`, `os build` and the runtime publish door for a flow, an email-field recipient whose field the trigger object does not declare (`notify-email-field-unknown`), one whose field is not `email`-typed (`notify-email-field-not-email`), and one in a flow that hands its run no record of a known object — a schedule, an inbound api call, a record trigger that names no object (`notify-email-field-no-trigger-record`). A trigger object another package declares is not judged.
2424

25-
This change is the protocol half. Delivery — resolving the field to an address, the outbound mail configuration, the per-address rate limit and the messaging-log entry — is the messaging service's half and lands separately; until it does, a run that reaches such a node fails at the node.
25+
This change is the protocol half. Delivery — resolving the field to an address, the outbound mail configuration, the per-address rate limit and the messaging-log entry — ships in the same release in `@objectstack/service-automation`.
2626

2727
Clause-②: yes (widening)
2828

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): Browse Marketplace says the catalog could not be reached when the runtime cannot reach it, instead of "the catalog answered with an error (HTTP 502)"
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **`@object-ui/app-shell`**: the runtime's marketplace proxy answers `502 MARKETPLACE_PROXY_FAILED` only when its own fetch to the catalog throws. The load-error panel picked its class line by status alone, so that answer read "The marketplace catalog answered with an error (HTTP 502)", with the raw transport text `fetch failed` in full view below it. The panel now reads the error code as well: that code, or no answer at all, reads "The marketplace catalog could not be reached.", and the raw text sits behind a **More** / **Less** disclosure with a copy button. A failure the catalog did answer, including a 502, 503 or 504 relayed from upstream, still names its HTTP status and shows the server's own text. The online hint is unchanged.
11+
12+
What to do: nothing.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/plugin-kanban': patch
3+
---
4+
5+
feat(plugin-kanban): the board dims the lanes a dragged card's `state_machine` closes and refuses a drop into one
6+
7+
Clause-②: yes
8+
Metadata-protocol: none
9+
10+
A board grouped by a field that an error-severity `state_machine` validation governs (the showcase Task Board's `status`, for one) used to accept a card dragged onto any column. The server refused the invalid move and the board rolled it back with a toast, and nothing during the drag marked the columns the card could not enter.
11+
12+
- **While a card is dragged**, every lane its column's value may not move to is dimmed and draws no drop highlight (`aria-disabled`, `data-kanban-drop-blocked`), on the flat layout and in every swimlane row.
13+
- **A drop into such a lane is refused on the client**, before the optimistic move and before any write: the card stays in its column. A cancelled drag puts the lanes back.
14+
- **Which lanes close** is read through `stateMachineNextValues` from `@object-ui/core`, the same read the record form and the grid's inline editor use. Only a rule on the board's own `groupBy` field that the server enforces on update closes a lane (`severity` `error`, `active`, `events` covering `update`). An undeclared from-state and the Uncategorized lane stay open.
15+
- `KanbanBoard` (the lazily loaded board both entry points render) takes a new optional prop, `canDropCard(fromColumnId, toColumnId)`. `ObjectKanban` derives it; `KanbanRenderer` passes none, so a host-mounted board is unchanged. The server still refuses an invalid move, and the mover's rollback stays. No new visible text.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/fields': patch
3+
---
4+
5+
fix(fields): a `user` field's person object is always `sys_user` — the user cell and the person picker no longer read an authored `reference`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A `user` field's target is a constant of its type: `@objectstack/spec` refuses a `user` field whose `reference` names any object but `sys_user`, and `referenceTargetOf` resolves every `user` field to `sys_user` whatever the field carries. The console now agrees with it in two places:
11+
12+
- **`UserCellRenderer`** (read-only cell): the field-level read gate on the person's name and avatar is keyed by `sys_user`. A stored object saved before the spec refused a foreign `reference` (for example `reference: 'crm_member'`) used to gate the cell by that object's policy; it is now gated by the `sys_user` policy, the object the runtime expands the field into.
13+
- **`UserField`** (person picker): the picker targets `sys_user` even when the field carries a foreign `reference`, so it offers users, matching what the runtime stores and resolves.
14+
15+
What to do: nothing for a spec-valid `user` field (no `reference`, or `reference: 'sys_user'`). A field that links to another object is a `lookup`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(storage)!: a record write naming a file its caller may not read is refused at the field
6+
7+
A record write that names a file its caller may not read is now refused at that field, instead of saving the id as written. The answer is one `400 VALIDATION_FAILED` whose `fields[]` names each such field with the ADR-0114 code `invalid_value`, the same envelope the public form door gives an anonymous submission that carries a file. It names the field only. An id that names no file of the acting organization gets the same answer, so the write cannot tell the two apart. Nothing is claimed, copied or written. System writes and writes with no caller context reference files as before, and an authorization-store outage still fails the write with its declared envelope.
8+
9+
A file governed by a record's rule also gets the record-governed link lifetime at the download door.
10+
11+
Clause-②: no (narrowing) — a record write that names a file its caller may not read, or an id no file of the organization has, was saved and is now refused. Nothing that was refused becomes accepted.
12+
13+
Metadata-protocol: none (a write-time refusal on file fields; nothing a metadata author writes changes)
14+
15+
<!-- adr-0087: not-required (no-migration-prescription) No metadata author's surface moves: no spec key or authorable spelling is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is which record writes the file-reference lifecycle accepts: a write naming a file its caller may not read is refused at the field. The remedy is naming a file the caller may read, or leaving the field empty, never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
16+
17+
**BREAKING** for a caller that writes a file id it may not read, shipped as `patch` because the metadata protocol does not move.
18+
19+
- **Before.** The write saved, and the field kept an id its readers could not use.
20+
- **Now.** The write is refused with `400 VALIDATION_FAILED`, naming the field with `invalid_value`.
21+
- **Fix.** Name a file the caller may read (upload it, or choose one they can open), or leave the field empty.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os dev` compiles the config before it boots, so the served connector `baseUrl` is the one dispatch calls
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `os dev` booted whatever `dist/objectstack.json` already held, and compiled only when it was missing or `--compile` was passed. A host config's dev boot serves the stack from two copies: the config module the server evaluates (what a provider-bound connector is materialized from, so what dispatch calls) and the artifact (what `/api/v1/meta/<type>` serves). A config value read from the environment, such as `examples/app-showcase`'s self URL built from `OS_PORT`, then differed between them. With `OS_PORT=41302` over an artifact built without it, `GET /api/v1/meta/connector/showcase_status_api` served `http://127.0.0.1:3000` while the connector called port 41302.
11+
- **Now.** `--compile` is on by default: `os dev` compiles `objectstack.config.ts` into the artifact it serves, in its own environment, before the server starts, so both copies come from one evaluation. The watch loop already recompiled in that environment.
12+
- **`--no-compile`** boots the existing artifact as built, as a bare `os dev` did before (it is still compiled when missing, and the startup warning still says when it is older than the sources). A pinned artifact (`--artifact`, `OS_ARTIFACT_URL`, a remote `OS_ARTIFACT_PATH`) is never rebuilt. With no `objectstack.config.ts`, an existing artifact boots as before.
13+
- `os serve` and `os start` are unchanged.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix: a sandboxed hook's or action's business refusal is logged once, at info, without a stack
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
When a sandboxed hook or action body refuses on purpose, for example a state-machine hook answered as `422 INVALID_STATE`, the server log used to get two `ERROR [BodyRunner]` lines with full stacks: one at the hook and one again at the action that triggered it. An operator watching for errors could not tell a refused click from a crashed hook.
11+
12+
Now a throw that the HTTP doors answer in the 4xx band writes one `info` line, `[BodyRunner] sandboxed hook refused` (or `action refused`), naming the hook or action, its declared `code` and the status, with no stack. The action does not repeat a refusal its hook already logged. A throw that becomes a 5xx (a crash, a denied capability, a declared 5xx) still logs `error` with its stack, at both the hook and the action as before.
13+
14+
The refusal-or-fault split is read from the same error table the HTTP doors answer with (`mapDataError`), so the log level follows the status the caller saw. The HTTP status, body and message of every refusal are unchanged, and every throw is still rethrown.

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
137137
- 一个标签存在当且仅当有具名读者;`pm:blocking` 即其推导缓存,读者以词表脚本注为权威。
138138
- 跨仓解锁判据是消费方可安装,⛔ 不是上游已合并;探针:装上的包是否接受新键/新行为。
139139
- 以发布包消费上游的仓,解锁放回前先验修复已进可安装的发行版。
140-
- 未发版 ⇒ 转 `pm:on-hold` + `Restart-when:` 加消费方安装面判据,⛔ 不回 `pm:queue`。
141-
- 该 hold 评论预写唤醒后的派发形状;pin 消费的仓不适用:pin 移动即可安装,走 pin 滞后读数。
140+
- 未发版 ⇒ 转 `pm:on-hold`;`Restart-when:` 写安装面判据与唤醒后派发形状,⛔ 不回 `pm:queue`。
142141
- 状态变更不过夜:标签无评论、assignee 无认领评论、结论只在 chat,都是半状态。
143142
- 半状态在结束会话(含限流悬挂)前补齐成对或回滚半边;report-only 巡查见守卫索引。
144143
- 等待他座位也是状态,写在卡上才存在(等谁、自何时),⛔ 不留在会话记忆。
@@ -162,8 +161,9 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
162161
| `domain:devx` | `sdui-parser`、`packages/qa`、`content/docs`、`apps/docs`、`docs/qa`、`.githooks`、`docker/README.md`、`examples/**` 仅测试基建面;`packages/lint`、`scripts`(门禁类)、`.github/workflows`(接线)三者按锚定规则归 `domain:spec`,门禁按 SUBJECT:governed 面归 skills,代码/文档质量归本域 |
163162
| `domain:skills` | governed 面全量(含本文件;统一定义见「governed 面统一定义」行);非门禁的 `scripts/pm/**`(PM 循环工具);governed 面的治理执行文件:`.github/CODEOWNERS` + SUBJECT 是 governed 面本身的门禁/审计(现为 `scripts/pm/check-governed-merges.mjs`) |
164163
| `domain:spec` | `packages/spec` 整包:schema 形状、`contracts/**`、退役行为半边、strictness 台账;describe/JSDoc/墓碑散文/错误 guidance 与 alias 表;`packages/spec/scripts/**`、`packages/spec/docs/**` 及按锚定规则的例外归本域的工具链(域边界枚举与席内分派见 `references/lanes/spec.md`) |
164+
| `domain:ui` | `ui/**` 全部代码:`@object-ui/*` 库与 `ui/apps/*`、`ui/packages/types` 与 schema 语料、`ui/scripts` 与门禁工具、文档随所记录的面走;`packages/console`(dist 由 `pnpm console:build` 生成 ⛔ 不手改);协议恒归 `domain:spec`,受管面归 `domain:skills`;详见 `references/lanes/ui.md` |
165165
| `domain:cloud` | 云服务旅程,停放(加 `status:parked`,sweep 免扫)待 cloud PM 迁移,本仓不派 |
166-
| (无固定归属,按落点分诊) | `packages/apps/*`、`packages/console`(dist 由脚本生成 ⛔ 不手改;UI 缺陷走 `repo:objectui`)、`examples/*`(归它演练的子系统)、`docs/audits/**` |
166+
| (无固定归属,按落点分诊) | `packages/apps/*`、`examples/*`(归它演练的子系统)、`docs/audits/**` |
167167

168168
- 表未覆盖的包首次分诊时归类并走 PR 更新本表;新增或退役 `domain:*` 必须同批改本表。
169169
- 退役 `domain:*` 只退流通,对象保留;`domain:engine`/`domain:services`/`domain:cli` 读作 `domain:server`。

0 commit comments

Comments
 (0)