Skip to content

Commit 1699ec5

Browse files
committed
Merge origin/main after #24667 (step 4) into the ceremony branch
Conflicts and restatements, the ceremony being open on this branch: - runtime boot guard, its unit, kernel and dogfood pins: #24682's 18.0 "fresh installs only" refusal gives way to the marker-based refusal that names the ceremony's plan and apply. - cli.mdx: the open-ceremony text kept; #24678 and step 4's "until then the ceremony refuses" sentences restated; #24722's audit-mail-bodies row and section kept beside it. - B2: #24661's seven 18.0 upgrade-guide entries prescribe the ceremony again (the ups entry keeps the full apply), pinned in organization-ownership-upgrade-entries.test.ts. - metadata-lifecycle, metadata-service, capabilities, permission-sets: the 18.0 deferral restated. - platform-core.v18-boot-refusals revision 5. - objectql: WALLED_ORGANIZATION_LESS_BRIDGE, isWalledOrganizationLessBridgeObject and the engine reader are deleted (sys_secret and sys_flow_credential are column-less here); the refusal names systemFields: { tenant: false }. The tripwire is deleted; the flow-credential walled pin is kept, renamed walled-flow-credential-write.test.ts. - #24688's principal x door matrix restated over a ceremonied fixture. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
2 parents edb2db1 + 580fd4e commit 1699ec5

316 files changed

Lines changed: 16626 additions & 1637 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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
feat(objectql)!: under a walled posture (`group` / `isolated`), a system insert into a platform object that names no organization is refused; it no longer lands with `organization_id` NULL
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A validity narrowing at the engine's system-insert door: no key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. No export, type or error code is removed or renamed: `PLATFORM_OBJECT_TENANCY`, `isPlatformObjectOutOfTenantAuditScope` and `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` keep their shapes; two exports are added, `WALLED_ORGANIZATION_LESS_BRIDGE` and `isWalledOrganizationLessBridgeObject`, the temporary bridge named below. The remedy is a run-time one, stated below. -->
10+
11+
**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D9, D11). This is the walled half of the change whose `single` half shipped in 18.0.
12+
13+
- **Before.** Under `group` or `isolated`, a system-context insert (`isSystem`, no `tenantId`) into a `sys_` / `cloud_` / `ai_` object that carries an organization column, but that the tenancy inventory had not admitted, skipped the organization rule. The row landed with `organization_id` NULL, a row no organization owns and the wall hides from every organization.
14+
- **Now.** That insert goes through the same decision every application object already gets under a wall: no organization is derivable, so it is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (status 500, reason `walled-posture`), and nothing is written. Apart from the two tables named below, the posture no longer changes which objects the rule covers.
15+
- **Unchanged.** A write that carries an organization, on the execution context or on the record, is never re-stamped or refused. Objects with no organization column (`systemFields: { tenant: false }`), objects declaring `tenancy: { enabled: false }`, and federated objects are outside the rule. `sys_secret` and `sys_flow_credential` keep their organization-less system inserts under a wall, as before: a global-scope encrypted setting, a datasource credential and a flow credential are deployment-level, and both tables drop their organization column in a later 18.x release, which retires this bridge (`WALLED_ORGANIZATION_LESS_BRIDGE`). Updates are untouched. The engine's diagnostics-only tenant-audit mute still reads the inventory.
16+
17+
**The remedy.** If a refusal names your insert, carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId }, { context: { isSystem: true } })`, or `{ context: { isSystem: true, tenantId: orgId } }`. If the row is deployment-level by design, the object should declare no organization column: `systemFields: { tenant: false }`.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-webhooks': minor
3+
---
4+
5+
feat(plugin-webhooks): under `group` and `isolated`, a declared webhook is deployment-wide, as it is under `single`; it receives every organization's events, and each delivery is stamped with its event's organization
6+
7+
Clause-②: yes (widening)
8+
9+
A webhook declared with `defineStack({ webhooks })` belongs to the deployment under every tenancy posture (ADR-0131 D2/D6, the epic's ruling A on the walled shape of a declared webhook).
10+
11+
- **Before.** Under a walled posture (`group` / `isolated`) a declared webhook had no organization, so the dispatcher refused every organization-stamped event it matched. It logged one warning per webhook and delivered nothing.
12+
- **Now.** Switched on, it receives every organization's matching events, as under `single`. Each `sys_http_delivery` row is stamped with its event's organization, so an organization reads only its own deliveries. The switch is unchanged: one deployment-level `sys_metadata_activation` row through `POST /api/v1/security/_activation/webhook/<name>`. Under a wall only the platform operator may flip it; an organization administrator gets `403 PERMISSION_DENIED`.
13+
- **Unchanged.** Webhooks an organization authors as `sys_webhook` rows still receive only their own organization's events.
14+
15+
**If you relied on the old behaviour.** A walled deployment that declared a webhook it did not want delivered across organizations should switch it off: `POST /api/v1/security/_activation/webhook/<name>` with `{ "enabled": false }`, as the platform operator.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
feat(cli): `os serve` (and so `os dev` and `os start`) boots a walled tenancy posture again; `group` and `isolated` are supported on fresh installs
6+
7+
Clause-②: yes (widening)
8+
9+
18.0 refused a walled posture at boot, because the walled half of the platform-object organization rule had not shipped. It ships in this release (see the `@objectstack/objectql` changeset on walled system inserts), so the refusal is removed.
10+
11+
- **Before.** With `OS_TENANCY_POSTURE=group` or `isolated`, or `OS_MULTI_ORG_ENABLED=true`, `os serve` exited `1` with a FATAL `NOT_IMPLEMENTED` before it read a config. `os doctor` printed a `Tenancy posture` warning saying so, and the unrecognised-posture fix list marked both walled values as refused in 18.0.
12+
- **Now.** A walled posture boots like `single`, under the same prerequisites as before 18.0: the enterprise `@objectstack/organizations` runtime, and `OS_PLATFORM_OWNER_EMAIL`. `os doctor` reports no row for a valid posture, and the fix list offers `group` and `isolated` without the 18.0 note.
13+
- **Unchanged.** An unrecognised posture is still refused at boot. Upgrading an existing database still waits for the upgrade ceremony, which still refuses in this release.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/plugin-webhooks': minor
3+
---
4+
5+
feat(plugin-webhooks)!: a `sys_webhook` row can no longer take a declared webhook's name — an insert of one, or a rename onto one, is refused with `409 RESOURCE_CONFLICT`
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A write-door narrowing on one platform table: no spec key, export of the spec, option or stored metadata shape is added, removed or renamed, so `objectstack migrate meta` has nothing to rewrite; rows already stored are never touched, and the remedy is a run-time one, stated below -->
10+
11+
**BREAKING** accept-set narrowing, shipped as `minor` under the launch-window convention for breaking changes (ADR-0131 D6 regime C).
12+
13+
A declared webhook (`defineStack({ webhooks })`) is read-only, and a `sys_webhook` row holding its name withholds the declaration for the whole deployment. Under a walled posture one organization's same-name row would silence the declaration for every organization. So no write may take a declared name, as declared sharing rules already do.
14+
15+
- **Refused now.** A non-system insert into `sys_webhook` whose `name` is declared, switched on or off, and an update that changes `name` onto a declared one. Every door is covered (Setup, the `/data` API, scripts, AI tools): the refusal is `DeclaredWebhookNameConflictError`, `code: 'RESOURCE_CONFLICT'`, `status: 409`, naming the declared webhook.
16+
- **Unchanged.** System writes (the platform's own sweeps and migrations). An update that keeps its name, so the rows an 18.0 install's seeder wrote, and the ones an administrator customised there, stay editable as before. Names nothing declares.
17+
18+
**The remedy.** To change a declared webhook, author your own `sys_webhook` row under another name, and switch the declared one off: `POST /api/v1/security/_activation/webhook/<name>` with `{ "enabled": false }`.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
---
4+
5+
feat(plugin-security): organization administrators assign positions and permission sets to their own members, inside the organization boundary
6+
7+
Clause-②: no
8+
9+
ADR-0090 D12 (as amended for ADR-0131 D3) names three kinds of administrator. This release enforces the second one, the organization administrator: a holder of `organization_admin` or `organization_admin_no_bypass` for the active organization who is not a platform administrator.
10+
11+
- **What an organization administrator may now do.** Create and remove `sys_user_position` and `sys_user_permission_set` rows of its own organization, for that organization's members. Every permission set the row distributes must be inside the organization boundary: the granted set, or each set the assigned position carries (including the set of the position's own name). A set is inside when its `objects['*']` entry carries neither `viewAllRecords` nor `modifyAllRecords`, it has no `adminScope`, and every name in its `systemPermissions` is a capability declared `scope: 'org'`. `admin_full_access`, both organization-administrator sets and the three MCP agent ceilings are outside by name. An assignment row is never edited in place: remove it and create the one you mean. A row inserted with no organization lands in the caller's organization.
12+
- **What is refused, and how.** An out-of-boundary write answers `403 PERMISSION_DENIED`, and the message names the set and the condition it failed. The same refusal covers a row of another organization, removal of an organization-less row, a business unit of another organization, an edit in place and a filter delete. Assigning a user who is not a member of the organization is refused `400 VALIDATION_FAILED` as before, for every writer. So is assigning a built-in identity position (`platform_admin`, `org_owner`, `org_admin`, `org_member`). Organization membership rows stay platform-administrator-only.
13+
- **Overrides.** `organization_admin`, and so `organization_admin_no_bypass`, now carries `allowCreate` and `allowDelete` on both assignment tables. `allowEdit` stays `false`.
14+
- **The two doors.** `GET /security/my-delegable-scope` reports an organization administrator with `isTenantAdmin: false`, every unit of its organization as placeable, and only in-boundary positions as assignable. The walled `organization_admin` was reported as tenant-level before. Explaining another user's access now admits an organization administrator explaining a member of its own organization.
15+
- **Unchanged.** Platform administrators keep today's behaviour. Delegated (`adminScope`) administrators keep D12's subtree, allowlist and strict containment. Everyone else is denied as before. No spec key, no endpoint and no membership-model change.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
fix(platform-objects)!: `sys_organization` no longer declares `change_slug`, whose door only the cloud orchestrator mounts (#24318)
6+
7+
Clause-②: no (narrowing) (a platform object stops declaring an action whose door exists only in cloud; owes a `CONTRACT_REVIEW_TIER` review on the narrowing face before the queue, and the `repo:cloud#2` seat's pointer before landing)
8+
9+
`sys_organization` declared a `change_slug` action ("Change Slug") at `record_header`, gated on `multiOrgEnabled`, whose target, `/api/v1/cloud/organizations/{id}/change-slug`, no package in this repository serves. So on every open-source composition with multi-organization on, the organization record page offered Change Slug in its `⋯` overflow, and the click answered `404 ENDPOINT_NOT_FOUND`. Measured on a booted multi-organization showcase.
10+
11+
The action now belongs to the composition that serves its route. That composition declares `change_slug` as a top-level action bound by `objectName: 'sys_organization'`, and the registry attaches such an action to the served object, whichever package registers first.
12+
13+
**BREAKING — what moves for consumers.** A declared action leaves a published platform object, shipped as `minor` under the launch-window convention for breaking changes (pre mode was exited at `c46b5f14cf`, so the launch-window guard is armed and a breaking change ships as `minor`).
14+
15+
- FROM: `GET /api/v1/meta/object/sys_organization` served `change_slug` on every composition, and the four generated locale bundles carried its `label`, `description` and `successMessage`.
16+
- TO: the object serves it only where a composition binds it. The open-source record header offers Invite User, Set Active and Rename Organization inline, and Leave Organization in the overflow; each of them is a better-auth door this composition mounts.
17+
- Fix: cloud declares its own `change_slug`, the same declaration bound by `objectName: 'sys_organization'`, with its own translations. An open-source composition changes an organization's slug through `update_organization` (better-auth's `organization/update`, list row), as before.
18+
19+
**Unchanged.** `rename_organization` (name and logo, no slug) and `update_organization` (name, slug and logo, list row only) keep their declarations, gates and order. No stored row moves.
20+
21+
<!-- adr-0087: not-required (no-migration-prescription) a platform object stops declaring one action whose door exists only in another composition: no authorable spec key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. The repair is the serving composition's own declaration of the action, which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this declaration (not already-registered); and the change is an object's declared action, not a runtime interface, a type surface or an export (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: `PUBLIC_AUTH_FEATURES.multiOrgEnabled.gatedInputs` no longer lists `sys_organization.actions.change_slug` (#24318)
6+
7+
Clause-②: no (narrowing) (a platform object stops declaring an action whose door exists only in cloud; owes a `CONTRACT_REVIEW_TIER` review on the narrowing face before the queue, and the `repo:cloud#2` seat's pointer before landing)
8+
9+
`gatedInputs` is the census of platform-object inputs gated on a public auth feature, which the platform objects' completeness guard holds equal to their declarations in both directions. `sys_organization` no longer declares `change_slug` (its door is the cloud orchestrator's, and the composition that serves it declares the action), so its row leaves the census.
10+
11+
**BREAKING — what moves for consumers.** One string leaves a published constant, shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
- FROM: `PUBLIC_AUTH_FEATURES.multiOrgEnabled.gatedInputs` listed seven `sys_organization` actions, `'sys_organization.actions.change_slug'` among them.
14+
- TO: it lists six: `create_organization`, `update_organization`, `rename_organization`, `delete_organization`, `set_active_organization` and `leave_organization`.
15+
- Fix: none for an action declared outside the platform objects. The census is not a gate on other packages: a top-level action bound to `sys_organization` that declares `requiresFeature: 'multiOrgEnabled'` is lowered to the same `features.multiOrgEnabled != false` predicate and served with it, measured on a booted composition.
16+
17+
**Unchanged.** The `multiOrgEnabled` key, its surface (`crud`) and its semantics (`default-on`); every other feature's row. The constant is declared `as const`, so its literal type loses the one tuple member too.
18+
19+
<!-- adr-0087: not-required (no-migration-prescription) one path string leaves a published census constant that only the platform objects' own completeness guard reads: no authorable spec key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this row (not already-registered); and the export stays, its declared entry type unchanged; the `as const` literal tuple loses one member, which nothing in this repository names (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
---
4+
5+
Clause-②: no
6+
7+
The `source` describe on `MetadataEventSchema` no longer lists `"fs"`, `"studio"`, `"rest"`, `"ai"` and
8+
`"git-import"` as origin labels: of those, only `fs` is written by any producer (`FileSystemRepository`),
9+
and no write path stamps an authoring surface there. It now says what the field is: the write path that
10+
produced the event, an open label its writer stamps rather than an enum to pick from or match against, for
11+
example `protocol.saveMetaItem`, `protocol.publishMetaItem`, `migrate-stored`, `sys-metadata-repo`
12+
(`SysMetadataRepository`'s default) or `fs`. The field stays `z.string()`, so no key, type, export or
13+
accepted value changes, and every repository emits exactly what it emitted before.
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.

0 commit comments

Comments
 (0)