Skip to content

Commit 2fdf7b1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24647-explain-authored-row-write
2 parents d2e2454 + 8e14ce0 commit 2fdf7b1

67 files changed

Lines changed: 2911 additions & 108 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): a delegated agent's own leg does not collect an object's declared default row scope (#24341)
6+
7+
Clause-②: no
8+
9+
An agent acting on behalf of a user (ADR-0090 D10) is judged on two legs that the read door ANDs together: its own sets, which are a capability ceiling and carry no row policy, and its delegator's sets. Since the row-policy collection began applying an object's declared `defaultRowScope` where the held sets carry no row policy, the ceiling's silent leg collected that default too. Where the delegator's own leg is wider than the default, the agent then read fewer rows than its delegator. The platform administrator is the case in point: the administrator's own leg skips row policies on the identity objects, so an agent acting for the administrator would have read only the administrator's own rows there, where it read every row before.
10+
11+
The collection no longer adds an object's default on a delegated principal's own leg, so a ceiling's silence is not read as the object's default. The delegator's leg collects the default exactly as any principal's does, and a principal that acts for no one keeps the default whenever its sets are silent.
12+
13+
What moves, against `18.0.0-next.5`: on an object that declares `defaultRowScope`, an agent acting for a user reads the rows its delegator's side admits, and its explain output names no object default on the agent's side. Before this release's reader, that was already so.
14+
15+
**Unchanged.**
16+
17+
- A principal that acts for no one, a human or otherwise, collects an object's default when its sets carry no row policy for the object and operation, as before.
18+
- The delegator's side, the superuser bypass, and every authored policy are judged as before.
19+
- No export, option or type moves.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
fix(objectql)!: the registry refuses an `extend` contribution that carries `defaultRowScope` (#24341)
6+
7+
Clause-②: no (narrowing)
8+
9+
An object's default row scope is declared by the object's owner alone. `SchemaRegistry.registerObject` now refuses an `extend` contribution that carries `defaultRowScope`, whatever its value and whatever the target, with `INVALID_METADATA` (`status: 422`) and the error name `ExtensionDefaultRowScopeRefusedError`, naming the object and the extending package. It is refused before the contribution is recorded, so nothing of it lands, and the owner's declaration stays in force.
10+
11+
Every value is refused because no narrower line holds. Whether one row predicate admits more rows than another cannot be decided in general, so a "loosening" cannot be told from a "narrowing". And the extension merge never carried the key onto the object, so an accepted one restricted nothing while its author believed it did.
12+
13+
**BREAKING — what moves for consumers.** This is an accept-set narrowing on the registry's `extend` door, shipped as `minor` under the launch-window convention for accept-set narrowings.
14+
15+
- FROM: `registerObject(def, packageId, undefined, 'extend')` with `def.defaultRowScope` set registered the contribution and dropped the key.
16+
- TO: it throws `INVALID_METADATA` (`422`, `ExtensionDefaultRowScopeRefusedError`).
17+
- Fix: remove `defaultRowScope` from the extension. To scope the rows the holders of a permission set read on the object, give that set a `rowLevelSecurity` policy for it.
18+
19+
**Unchanged.**
20+
21+
- An `extend` contribution that leaves the key out, or sets it to `undefined`, registers and folds as before. The one shipped `extend` of a platform object in this repository (`@objectstack/plugin-auth`'s `sys_sso_provider` extension) registers, measured on a booted composition.
22+
- A manifest's `objectExtensions` entry never reaches this refusal: its schema is strict and refuses the key at authoring, and `ObjectQL.registerApp` copies a closed key list off each entry.
23+
- The owner's own `defaultRowScope` registers and re-registers as before.
24+
- No export moves: the error class is not exported, and its code is the existing `INVALID_METADATA`.
25+
26+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at the registry's extend door of a key the extension merge never carried: no authorable key, spelling, export or stored shape moves, and the manifest path already refuses the key at authoring. The repair is removing the key from a programmatic extend call, which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this door (not already-registered); and the change is a door verdict, not an interface, a type or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/plugin-audit': minor
4+
---
5+
6+
feat(platform-objects)!: the platform identity objects declare their default row scope (#24341)
7+
8+
Clause-②: no (narrowing)
9+
10+
Twenty-three platform objects now declare `defaultRowScope`, each with the row policy the shipped permission sets of `@objectstack/plugin-security` carry for it, moved verbatim: the seven SCIM projections (`sys_scim_user`, `sys_scim_subject`, `sys_scim_projection_grant`, `sys_scim_identity_tombstone`, `sys_scim_group`, `sys_scim_group_member`, `sys_scim_connection_binding`), `sys_verification` and `sys_jwks`, `sys_organization`, `sys_user`, `sys_session`, `sys_account`, `sys_team_member`, `sys_two_factor`, `sys_user_preference`, `sys_api_key`, `sys_device_code`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, `sys_oauth_consent`, `sys_oauth_application` (`@objectstack/platform-objects`) and `sys_audit_log` (`@objectstack/plugin-audit`, which owns the ledger). `sys_user` is the one merge: the key holds one policy, so its two shipped policies are joined into `id == current_user.id || id in current_user.org_user_ids`, a `select` policy. The platform-global objects (`sys_metadata*`, `sys_job*`, `sys_migration*`, `sys_platform_setting`, `sys_presence`, `sys_flow_dispatch`) declare none.
11+
12+
The security plugin's row-policy collection applies an object's declared default when the permission sets the caller holds carry no row policy for that object and operation. The shipped sets still carry the same policies, so a holder of them reads exactly what it read before.
13+
14+
**BREAKING — what moves for consumers.** This narrows what a principal reads on these objects when its sets carry no row policy for them, shipped as `minor` under the launch-window convention for accept-set narrowings.
15+
16+
- FROM: a principal holding no platform baseline (a deployment that sets the security plugin's `fallbackPermissionSet: null`) whose sets grant read on an identity object and carry no row policy for it read every row of that object.
17+
- TO: it reads the object's declared default: its own rows (`user_id == current_user.id`); on `sys_user`, its own row plus the members of the organisation it acts in; on `sys_organization`, its own organisation; no row on `sys_verification`, `sys_jwks`, `sys_scim_group`, `sys_scim_group_member` and `sys_scim_connection_binding`; on `sys_audit_log`, its organisation's entries where organisation scoping is on (the platform's tenant policies, this one included, are stripped where it is off).
18+
- Fix: to give such a principal more rows, give one of its permission sets a `rowLevelSecurity` policy for the object. A policy a held set carries is used as authored, and the default is not added to it.
19+
20+
**Unchanged.**
21+
22+
- A deployment with the platform baseline (`member_default`, the default) serves every human exactly what it served before, on every one of the 23 objects and every operation: each human holds the baseline, and the baseline carries a policy for each object and operation. Measured on a booted composition with a member, a viewer, an organisation admin, a user in no organisation and the platform administrator, before and after the declarations.
23+
- An MCP agent acting for a user reads the same rows, and gets the same explain output, as before. Its ceiling set carries no row policy, and the security plugin does not collect an object's declared default on a delegated principal's own side of the intersection (see the `@objectstack/plugin-security` entry), so the agent's rows stay bounded by its delegator's side alone. Measured on a booted composition, before and after the declarations, for the agents of a member, a viewer, an organisation admin and the platform administrator; the platform administrator's agent still reads every row.
24+
- The platform administrator's superuser bypass is decided before any row policy is collected.
25+
- No export, option or stored shape moves.
26+
27+
<!-- adr-0087: not-required (no-migration-prescription) declarations of a row scope on platform objects, each the policy the shipped permission sets already carry: no authorable key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. What narrows is the rows a principal with no baseline and no row policy of its own reads, and the repair is a deployer's choice of a `rowLevelSecurity` policy, which no ledger entry can derive. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id covers row scopes (not already-registered); and the change is a runtime row scope, not an interface, a type or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
feat(metadata-protocol): `getMetaItemForExecution`, the by-name metadata read without the serving decorations
6+
7+
Clause-②: yes (widening): `ObjectStackProtocolImplementation` gains one public method on its published declaration. Measured on the built `dist/index.d.ts` before and after: the export list is unchanged (131 names), and the class gains `getMetaItemForExecution` and a private member. Every other member signature is byte-identical, `getMetaItem` included.
8+
9+
- **What.** `getMetaItemForExecution(request)` takes `getMetaItem`'s request and answers its envelope. It shares the same sources, the same overlay cache entry, merge, lock and governance, the same disabled-package refusal (`404 OBJECT_PACKAGE_DISABLED`) and withholding, and the same `NO_DRAFT` refusal. The item comes back without the serving decorations: no `_diagnostics` (the full schema `safeParse` of `computeMetadataDiagnostics`, and a view's cross-document reference diagnostics), no per-type credential redaction, and no `registration`. It is the by-name twin of `getMetaItemsForExecution`.
10+
- **Why.** It is for an in-process caller that acts on a definition and serves none of it. The REST API-exposure gate reads the object behind every `GET /api/v1/data/OBJECT` to judge `enable`. On the served face, that read paid a full diagnostic of the object's definition per request: 11.8% of a measured hosted CRM read's CPU, and more on larger objects.
11+
- **Never served.** Do not serve what it returns from a door that answers a caller (HTTP, MCP, an export, a log line). Every such exit serves `getMetaItem`, whose redaction is why the two faces are separate.
12+
- **Unchanged.** `getMetaItem`, `getMetaItemCached` and the authoring read answer byte-identical JSON. This was measured over 480 served answers, comparing the previous build and this one: types, drafts, the overlay cache, refusals and withholding.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/rest': minor
3+
---
4+
5+
perf(rest): `GET /api/v1/data/OBJECT` no longer computes the object's metadata diagnostics; the API-exposure gate reads the protocol's execution face
6+
7+
Clause-②: yes (widening): the exported `RestProtocol` type gains one optional member, `getMetaItemForExecution?`. Measured on the built `dist/index.d.ts` before and after: the export list is unchanged (81 names), and every other member signature is byte-identical. The member is optional, so nothing a protocol must provide changes.
8+
9+
- **What.** The API-exposure gate on the `/data` doors (`enforceApiAccess`) reads the object it judges through `getMetaItemForExecution` when the protocol has it, and through `getMetaItem` otherwise. Both are the by-name read with the same sources, overlay cache and governance. The execution face skips the serving decorations, so a `/data` request computes no `_diagnostics` (a full schema `safeParse` of the object's definition) to read one `enable` block. On a fixture object with 60 fields, a warm by-name read took about 1.0 ms on the served face and about 32 µs on the execution face; this was measured on a shared box, and the ratio is about 0.03.
10+
- **Unchanged.** Every `/data` answer: the same `404`/`405` verdicts and `allowed` set, a thrown read still fails open and logs, and an object of a disabled package still gets the data plane's `404 OBJECT_PACKAGE_DISABLED`. `GET /api/v1/meta/object/NAME` still serves `_diagnostics`.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/plugin-audit': minor
4+
'@objectstack/plugin-approvals': minor
5+
---
6+
7+
fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with `manage_platform_settings`, and the audit mirrors never copy them in clear
8+
9+
Clause-②: yes (narrowing). A principal without `manage_platform_settings` (an organization administrator, a member) loses read access to the body of every mail-log row.
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) A read-access narrowing on two existing fields (sys_email.body_text and body_html, and the same columns of sys_identity_email), plus a masked value in the audit copies and one object's free-text search set. No spec key, enum value, export, option, config field, response field or stored shape is removed, renamed or re-shaped, so there is no tombstone and nothing for `objectstack migrate meta` to rewrite. The three packages publish (not unpublished); no ADR-0087 id covers this behaviour and this diff adds none (not registered / already-registered); and the change is a behaviour change, not only an interface or type change (not runtime-interface-only / type-surface-only). -->
12+
13+
**BREAKING** (a read narrowing), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
An 18.0 install wrote its identity mail (password reset, account invitation, verification, magic link, email-change notice) into `sys_email`, stamped with the organization. Those bodies carry reset and sign-in links. `sys_email` had no read gate of its own, so an organization administrator could read them through the data API, global search, and the `sys_activity` / `sys_audit_log` mirrors. New identity mail goes to `sys_identity_email`, but its mirror rows still held the body in clear.
16+
17+
- **Before:** any reader of `sys_email` rows was served `body_text` and `body_html`, could filter and search on them, and found them in the activity and audit copies of the row. The audit writers copied both logs' bodies verbatim.
18+
- **Now:** `body_text` and `body_html` on `sys_email` and `sys_identity_email` declare `requiredPermissions: ['manage_platform_settings']`. A reader without the capability still gets the row: envelope, subject, delivery state and linkage. The body keys are left out. A filter on a body is refused (403). Global search does not match on a body. The activity and audit mirrors serve the reader no body. Their unpinned value-column queries are refused to such a reader, as for any reader withheld a field. The audit writers store `••••••••` for a non-empty body in every copy of a mail-log row (create, update and delete snapshots, and the activity row), as they do for a credential field. `sys_approval_request` free-text search no longer scans `payload_json`, so a reader withheld any field keeps working list and global search instead of a 403. A structured filter on the snapshot is unchanged.
19+
- **Unchanged:** the platform administrator reads every body. The mail pipeline (send, queue worker, outbox sweep, reclaim) reads as the system and is not narrowed. Rows and mirror copies written before this release keep what they stored; the mirrors still serve those copies only to a reader served the body.
20+
- **Why the whole column:** no column tells an 18.0 identity row apart from tenant mail. A magic link carries no `related_object`, and the log has no template column. So the gate covers every row's body, the organization's own mail included.
21+
22+
Remedy: a user who must read mail bodies (for example, to debug a notification template) needs a permission set that grants `manage_platform_settings`. An organization administrator still sees that each message was sent, to whom and with what result.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime): the organization-ownership boot refusals say what 18.0 supports for a v17 database and name no step of `os migrate organization-ownership`, which 18.0 refuses on every database
6+
7+
Clause-②: no (refusal text only: the refusal code `ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED`, the refused tables and the boot outcome do not move)
8+
9+
In 18.0, `os migrate organization-ownership` refuses plan and apply on every database (`NOT_IMPLEMENTED`): 18.0 supports fresh installs only. Three branches of the v18 boot refusal still told the operator to run that command's plan and its `--apply --plan-file PLAN --backup-taken`, so following them led to a second refusal.
10+
11+
Each branch now closes on the sentence `os migrate apply`'s drop gate and `os dev` already use: "18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path, and upgrade it there."
12+
13+
- **A table still in the v17 shape** (an object that declares no organization column, over a table that still carries `organization_id`). This branch already said that 18.0 does not upgrade a v17 database. It no longer follows that with the command's plan and apply steps.
14+
- **No table left in the v17 shape, and the ceremony's marker is not verified.** The refusal no longer prescribes a re-plan and a re-apply, which 18.0 cannot run.
15+
- **No table left in the v17 shape, and the marker could not be read.** The refusal now says to fix the read and start again: a database no ceremony ran on boots once the read answers. It no longer prescribes a re-plan and a re-apply.
16+
- **The column read itself failed.** The refusal still says to fix the read. It no longer offers the command as the alternative.
17+
18+
A development boot (`OrganizationOwnershipBootGuardPlugin({ dev: true })`, as `os serve --dev` composes it) says "a v17 development database" in place of "a v17 database". Before, it named the command with `--dev`.

‎.claude/skills/pm-dispatch/references/cross-repo-coordination.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
- 规则 3:联动杂事立单,不靠记忆。
3535
- 已验收 PR 的产物流向另一仓,接受该 PR 的执行席立即在消费仓立后续单,带 `Blocked-by:`。
3636
- 规则 4:纵向拆分,一个分诊 PM + N 个执行 PM;一人恰一车道,一车道可坐多席。
37-
- 分诊座位唯一生产:定级/路由/type/查重/shadow/`duplicate_of`;执行席永不定级或路由裸卡。
37+
- 多车道仓:分诊唯一生产定级/路由/type/查重/shadow/`duplicate_of`;执行席永不定级或路由裸卡。
3838
- 执行座位信任标签,只在本车道认领;误标 ⛔ 不自行改,挂 `pm:retriage` + 异议评论同笔。
3939
- 突发积压调频率或 `batch`,持续积压按仓拆席走 PR;⛔ 不跨车道借调。
4040
- 维护者直派通道:当面指挥的 PM 会话直接路由,只对明示指挥的卡成立。

0 commit comments

Comments
 (0)