Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
420fb8c
wip(plugin-webhooks): declared webhooks read from the registry; seede…
claude Oct 19, 2026
e44c642
test(plugin-webhooks): pin the registry read, the ledger switch and t…
claude Oct 19, 2026
8189a84
wip: liveness ledger re-anchored to the registry read; dogfood proof …
claude Oct 19, 2026
e412cb8
docs+ledger: the registry read named across spec comments, liveness, …
claude Oct 19, 2026
bdad70b
spec(liveness): state counts regenerated; isActive named at the dispa…
claude Oct 19, 2026
723af97
fix(plugin-webhooks): a declared webhook is deployment-wide when the …
claude Oct 19, 2026
e0a6dc9
fix(gates): tenant-audit census regenerated for the seeder's two reti…
claude Oct 19, 2026
6b3042a
test(plugin-webhooks): the fake engine refuses a WHERE combinator it …
claude Oct 19, 2026
820121c
fix(lint,showcase): the dead label/description verdict is carried to …
claude Oct 19, 2026
b378ff5
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
c3b1452
test(dogfood): the R2 harness pins one delivery per task change from …
claude Oct 19, 2026
9bce0aa
test(dogfood): the harness case switches the declaration on, so it ca…
claude Oct 19, 2026
7e580dc
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
fa6a837
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
b4ae8e6
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
a2e1e41
Merge remote-tracking branch 'origin/claude/issue-24529-8b0g-webhooks…
claude Oct 19, 2026
bea5245
feat(objectql)!: lift the platform-object system-insert exemption und…
claude Oct 19, 2026
31a9d26
feat(plugin-webhooks)!: refuse a sys_webhook insert or rename onto a …
claude Oct 19, 2026
f3782ae
chore(changeset): the walled platform-insert refusal and the declared…
claude Oct 19, 2026
758915b
test(plugin-security): walled fixtures carry the organization a walle…
claude Oct 19, 2026
37a67fa
docs(system-context): census row for the declared webhook name guard;…
claude Oct 19, 2026
2d88dd2
feat(plugin-webhooks)!: a declared webhook is deployment-wide under e…
claude Oct 19, 2026
75f6942
test(dogfood): the walled door pin holds manage_metadata, so only the…
claude Oct 19, 2026
a4cee54
chore(changeset): declared webhooks are deployment-wide under the wal…
claude Oct 19, 2026
45898db
feat(cli): os serve boots a walled posture again — the 18.0 refusal, …
claude Oct 19, 2026
fdfc842
Merge remote-tracking branch 'origin/main' into claude/issue-24580-wa…
claude Oct 19, 2026
1412586
test(plugin-sharing,plugin-auth): walled fixtures seed their organiza…
claude Oct 20, 2026
00191e7
fix(objectql): a cited walled bridge keeps sys_secret and sys_flow_cr…
claude Oct 20, 2026
98b45bd
chore(changeset): the walled insert changeset names the two bridged t…
claude Oct 20, 2026
3f839a9
Merge remote-tracking branch 'origin/main' into claude/issue-24580-wa…
claude Oct 20, 2026
ca7eca0
docs(qa): re-point the walled checklist legs and RUNNER.md's environm…
claude Oct 20, 2026
f73d889
test(service-datasource): alias objectql and platform-objects/system …
claude Oct 20, 2026
1e8338d
Merge remote-tracking branch 'origin/main' into claude/issue-24580-wa…
claude Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/15212-8b-walled-platform-insert-refused.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/objectql': minor
---

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

Clause-②: yes (narrowing)

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

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

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

**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 }`.
15 changes: 15 additions & 0 deletions .changeset/15212-declared-webhook-walled-deployment-wide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@objectstack/plugin-webhooks': minor
---

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

Clause-②: yes (widening)

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

- **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.
- **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`.
- **Unchanged.** Webhooks an organization authors as `sys_webhook` rows still receive only their own organization's events.

**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.
13 changes: 13 additions & 0 deletions .changeset/15212-walled-posture-boots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/cli': minor
---

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

Clause-②: yes (widening)

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.

- **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.
- **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.
- **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.
18 changes: 18 additions & 0 deletions .changeset/15212-webhook-declared-name-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/plugin-webhooks': minor
---

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`

Clause-②: yes (narrowing)

<!-- 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 -->

**BREAKING** accept-set narrowing, shipped as `minor` under the launch-window convention for breaking changes (ADR-0131 D6 regime C).

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.

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

**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 }`.
6 changes: 3 additions & 3 deletions content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ organization-ownership shape until the [ownership ceremony](#os-migrate-organiza
has run on it.

<Callout type="warn">
18.0 supports fresh `single` installs; upgrades and walled postures arrive in 18.x. In 18.0 the ceremony refuses, so `os dev` stops at step 2 with its
Fresh installs are supported under every tenancy posture; the upgrade path arrives in a later 18.x release. Until then the ceremony refuses, so `os dev` stops at step 2 with its
refusal and changes nothing.
18.0 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. For a local SQLite file,
Expand Down Expand Up @@ -1216,7 +1216,7 @@ where the data lives.
| `os migrate value-shapes` | Scan stored reference and structured-JSON field values against the platform's value contract, and record the deployment's migration flag when clean |
| `os migrate summary-nulls` | Backfill roll-up `count` / `sum` columns still stored as `NULL` on parent rows created before the insert-time seed. Repairs values; no flag, nothing depends on it having run |
| `os migrate comment-reactions` | Carry the emoji reactions a 17.x deployment stored in the retired `sys_comment.reactions` column into `sys_comment_reaction` records, list every stored value that does not read, and record the verified run that lets `os migrate apply --allow-destructive` drop the column |
| `os migrate organization-ownership` | The v18 ownership ceremony (ADR-0131 D10): write a plan of what each table's rows will become, then `--apply` that reviewed plan. A v18 server refuses to start until it has run. 18.0 supports fresh `single` installs; upgrades and walled postures arrive in 18.x. In 18.0 the command refuses. See [below](#os-migrate-organization-ownership) |
| `os migrate organization-ownership` | The v18 ownership ceremony (ADR-0131 D10): write a plan of what each table's rows will become, then `--apply` that reviewed plan. A v18 server refuses to start until it has run. Fresh installs are supported under every tenancy posture; the upgrade path arrives in a later 18.x release. Until then the command refuses. See [below](#os-migrate-organization-ownership) |
| `os migrate activity-changed-fields` | Stamp the two read-rule columns (`changed_fields`, `milestone_key`) on the `sys_activity` rows written before they existed. Until it runs, those rows are withheld from every reader but the system, so run it at once after upgrading. No flag; idempotent |
| `os migrate comment-parent-pair` | Stamp the parent pair (`object_name`, `record_id`) on the `sys_comment` rows written before it existed. Until it runs, a broad comment read past 2,000 candidate rows omits comments the reader may see, and its total agrees with the short list, so run it at once after upgrading. No flag; idempotent |
| `os migrate meta --stored` | Replay the metadata conversion chain over this deployment's `sys_metadata` rows and rewrite the ones still carrying a pre-protocol shape. Hygiene, not a gate — nothing depends on it having run |
Expand Down Expand Up @@ -1496,7 +1496,7 @@ has run. In the 18.x release that carries the upgrade path, `os dev` runs it on
its own for a local SQLite database (see [`os dev`](#os-dev)).

<Callout type="warn">
18.0 supports fresh `single` installs; upgrades and walled postures arrive in 18.x. In 18.0 this command refuses, plan and apply alike: it exits `1` with
Fresh installs are supported under every tenancy posture; the upgrade path arrives in a later 18.x release. Until then this command refuses, plan and apply alike: it exits `1` with
`NOT_IMPLEMENTED` (under `--json`, `code` beside `error`) and reads and
writes nothing. Keep a v17 database on a 17.x release until the 18.x release
that carries the upgrade path. The invocations below, and what the list after
Expand Down
2 changes: 1 addition & 1 deletion content/docs/deployment/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Read by `@objectstack/service-print` when `PrintServicePlugin` is composed; see
| `OS_AUTH_GOOGLE_ENABLED` | boolean | settings default | Settings env override for `auth.google_enabled`. Requires Google OAuth credentials from Settings or env. |
| `GOOGLE_CLIENT_ID` | string | — | Deployment-level Google OAuth client id for the open-source Google login implementation. |
| `GOOGLE_CLIENT_SECRET` | string | — | Deployment-level Google OAuth client secret for the open-source Google login implementation. |
| `OS_TENANCY_POSTURE` | `single` \| `group` \| `isolated` | derived from `OS_MULTI_ORG_ENABLED` | Which organization wall the authorization kernel enforces (ADR-0105 D1). `single` = no wall. `group` = `organization_id IN accessible_org_ids` — organizations are membership boundaries over one shared dataset, with union read access across every organization the caller belongs to. `isolated` = `organization_id = <active organization>`, the hard legal-entity wall (formerly spelled `multi`). Unset derives from `OS_MULTI_ORG_ENABLED` (`true` ⇒ `isolated`, else `single`), so existing deployments are unchanged. An unrecognized value **refuses to boot** rather than silently falling back to a posture with no wall. Both walled postures require the enterprise `@objectstack/organizations` runtime — without it the request resolves to `single` and boot is refused unless `OS_ALLOW_DEGRADED_TENANCY=1`. 18.0 supports fresh `single` installs; upgrades and walled postures arrive in 18.x. In 18.0 `os serve` refuses `group` and `isolated` at boot. |
| `OS_TENANCY_POSTURE` | `single` \| `group` \| `isolated` | derived from `OS_MULTI_ORG_ENABLED` | Which organization wall the authorization kernel enforces (ADR-0105 D1). `single` = no wall. `group` = `organization_id IN accessible_org_ids` — organizations are membership boundaries over one shared dataset, with union read access across every organization the caller belongs to. `isolated` = `organization_id = <active organization>`, the hard legal-entity wall (formerly spelled `multi`). Unset derives from `OS_MULTI_ORG_ENABLED` (`true` ⇒ `isolated`, else `single`), so existing deployments are unchanged. An unrecognized value **refuses to boot** rather than silently falling back to a posture with no wall. Both walled postures require the enterprise `@objectstack/organizations` runtime — without it the request resolves to `single` and boot is refused unless `OS_ALLOW_DEGRADED_TENANCY=1`. |
| `OS_MULTI_ORG_ENABLED` | boolean | `false` | Superseded by `OS_TENANCY_POSTURE`, and still honoured: `true` selects the `isolated` posture. When `true`, organization creation/switching UI is exposed. |
| `OS_AUTOMATION_SCHEDULED_WORK_ENABLED` | boolean | `false` | Whether this deployment runs **package-authored scheduled work**: time-triggered flows (a `type: 'schedule'` flow with a `config.schedule` cadence, and the `timeRelative` sweep) and packaged `defineJob` cron jobs. **OFF by default in every posture and every kernel** — a clock-driven workload's cost is a fact about the deployment, not about the flow, so an author cannot decide it and no metadata key asks them to. Accepts `true`/`1`/`on`/`yes` (case-insensitive); anything else, including unset, is off. While it is off nothing arms, and every such flow is listed by `getTriggerBindingAudit()` and the CLI startup summary as **disabled by deployment policy** — never as a binding failure, and nothing about the flow needs fixing. While it is on, what a time-triggered flow owes depends on the posture: under `isolated` it must declare `config.organization` on its start node or it is not armed; under `group` the declaration is optional and an undeclared sweep reads group-wide with each run acting as its own swept record's organization (a record-less cron there carries none, and its tenant-scoped writes are refused — declare one if it writes); under `single` it needs no declaration and its runs carry no organization. ⛔ Platform-internal scheduled work — approvals escalation, the lifecycle Reaper, the messaging dispatch loop, membership backfill — is **not** gated by this: the boundary is *authored by a package*, not *runs on the job service*. `os doctor` prints the effective value. |
| `OS_PLATFORM_OWNER_EMAIL` | csv | — | The deployment's platform administrators: one email address, or a comma-separated list. A caller resolves `PLATFORM_ADMIN` when their own stored `sys_user` row carries a declared address **and** reads email-verified. Comparison is trimmed and case-insensitive; duplicates collapse and blank entries are dropped. One **unparseable** entry refuses the **whole** variable rather than just that entry — the deployment then has zero configured administrators, loudly — because a silently narrower administrator set is the worse failure. **Required under the walled postures** (`group` / `isolated`): unset or blank there **refuses to boot**, since the first self-registrant is not promoted and no grant row is written. Unset under `single` is normal — that posture still promotes the first human account. Read live per resolution, so revocation is a config change plus a process reload; there is no runtime endpoint that changes it. See [First boot: create the admin](/docs/deployment/self-hosting#first-boot-create-the-admin). |
Expand Down
3 changes: 1 addition & 2 deletions content/docs/deployment/self-hosting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,7 @@ lock sign-up down via `OS_AUTH_SIGNUP_ENABLED` /
configuration.** First-registrant promotion is removed there: with
self-registration reachable, whoever posts to the sign-up endpoint first would
otherwise receive cross-tenant access. No grant row is written on those
postures, ever. Declare the administrators **before first boot** instead
(18.0 supports fresh `single` installs; upgrades and walled postures arrive in 18.x. In 18.0 `os serve` refuses a walled posture at boot):
postures, ever. Declare the administrators **before first boot** instead:

```bash
# one address, or a comma-separated list
Expand Down
12 changes: 4 additions & 8 deletions content/docs/deployment/tenancy-modes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,10 @@ it generalizes the two-valued *tenancy mode* of
whose `tenancy` service, membership lifecycle and degraded-tenancy boot guard all
still apply — widened to the three postures.

<Callout type="warn" title="18.0: single only">
18.0 supports fresh `single` installs; upgrades and walled postures arrive in 18.x. In 18.0 `os serve` (and so `os dev` and `os start`) refuses `group` and
`isolated` at boot with `NOT_IMPLEMENTED`, before it reads a config.
A database an 18.0 install creates takes the same 18.x upgrade path as a v17 one.
The refusal covers the `os` boot path only. The one-shot `os migrate`, `os verify`,
`os secret` and `os storage` boots and embedded hosts (`createStandaloneStack`,
`@objectstack/adapter-hono`) are not refused, and none of them is a supported
walled deployment in 18.0.
<Callout type="warn" title="Upgrades arrive in a later 18.x release">
Fresh installs are supported under every posture: `single`, `group` and
`isolated`. The upgrade path for an existing database arrives in a later 18.x
release, and a database an 18.0 install created takes the same path as a v17 one.
</Callout>

<Callout type="info" title="Renamed, not re-configured">
Expand Down
Loading
Loading