Skip to content

Commit 3d72157

Browse files
committed
Merge origin/main (a87d8be) into claude/issue-22146-guest-model-adr
Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK Co-authored-by: Claude <noreply@anthropic.com>
2 parents 73f031b + a87d8be commit 3d72157

64 files changed

Lines changed: 2562 additions & 411 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/plugin-security': patch
3+
---
4+
5+
A position saved through the metadata door no longer stops an app's declared positions from being seeded
6+
7+
Clause-②: no
8+
9+
Every seeding pass copies the app's declared positions into the `sys_position` catalog: the boot's pass and, on a walled deployment, the pass each new organization gets. That seeder read one source or the other: the engine registry alone whenever it held any position besides the six built-ins, otherwise the metadata service. A position a platform administrator saves with `PUT /api/v1/meta/position/:name` lives in the registry. So after one such save, every later pass seeded that position and none of the app's own. Measured on the showcase, walled: an organization created after the save held 7 positions (the six built-ins and the saved one) instead of 17.
10+
11+
The seeder now reads through the security catalog read (`createSecurityCatalogReader`). It reads both sources on every pass, and for a name both hold, the registry answers first.
12+
13+
- **New organizations** get the built-ins, every declared position and every door-saved one. Measured on the same walled showcase: 17 positions.
14+
- **Rows are written as before.** The door-saved position's row is unchanged: same name, label and description, `active: true`, `is_default: false`, no provenance stamp. For a name both sources hold, the registry's definition is still the one written.
15+
- **The six built-in positions** are still written only by the built-in seeder, with `managed_by: 'platform'`. Some deployments saved a definition under a built-in name before the six were declared, and that definition still shadows the declaration when the catalog is read. This seeder skips it by name, so it never reaches a row.
16+
- **A catalog read that fails** now writes nothing for that pass, and the boot reports it once as `[security] declared-position seeding failed`. That covers a source that cannot be read and a metadata list that lost a loader. Before, the seeder seeded whatever part it could read, without saying so.
17+
- **No grant changes.** A principal's permissions move only if it holds a position that is newly seeded into its organization.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/service-settings': minor
4+
'@objectstack/spec': minor
5+
'@objectstack/cli': minor
6+
---
7+
8+
feat(service-settings,platform-objects)!: the settings cascade's global rung moves to the tenant-less `sys_platform_setting`, and `sys_setting.scope` no longer declares `global` (ADR-0131 D7)
9+
10+
Clause-②: yes (narrowing)
11+
12+
<!-- adr-0087: registered sys-setting-global-rung-moved -->
13+
14+
**BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes (Changesets pre mode is not on yet).
15+
16+
A settings value for a key declared at `global` scope is a deployment-wide value: the mail transport, the SMS, storage, AI and knowledge providers, auth policy, the lifecycle retention defaults. It used to be a `scope: 'global'` row of `sys_setting`, a tenant-scoped table, where the injected `organization_id` column only ever held NULL and a walled posture hid the row from every reader. ADR-0131 D7 moves the rung out: it is now stored in a new platform object, `sys_platform_setting`.
17+
18+
- **`sys_platform_setting`** (registered by the settings service, beside `sys_setting`): one row per `(namespace, key)` for the deployment, with the `value`, `value_enc`, `encrypted`, `locked`, `locked_reason` and `updated_by` columns of a settings row, and no `scope`, `user_id` or `organization_id` (`systemFields: { tenant: false }`). It is governed by object permission: a generic data read needs `manage_platform_settings` (`requiredPermissions`), which platform administrators hold. Writes go only through the settings door.
19+
- **`SettingsService`** writes a global-scope key to `sys_platform_setting` and reads the cascade's global rung from there alone. Its `sys_setting` reads exclude `scope = 'global'`, so there is one source per rung. The cascade order, the lock semantics, `SpecifierScope` and the `source: 'global'` resolution value are unchanged. A global-scope change's `config_change` audit row names `sys_platform_setting` (`CONFIG_CHANGE_GLOBAL_OBJECT_NAME`); tenant- and user-scope changes still name `sys_setting`.
20+
- **`sys_setting.scope`** no longer declares the `global` option: no write produces such a row. `sys_setting_audit.scope` keeps it, because a global-scope change is still audited there.
21+
- **`os secret orphans` / `os secret rewrap`**: the settings family of the `sys_secret` reference union now reads both `sys_setting.value_enc` and `sys_platform_setting.value_enc`. Without that, every credential held at the global rung would read as unreferenced and be swept. An unreadable `sys_platform_setting` gaps the family, which refuses deletion.
22+
23+
**What moves for consumers.**
24+
25+
- **Existing databases — nothing moves automatically** (ADR-0131 D14). A `sys_setting` row at `scope = 'global'` is no longer read; until the v18 upgrade ceremony moves it, that key answers from its next rung or the manifest default. The ceremony moves each such row to `sys_platform_setting` by namespace and key, its `value_enc` handle included. An encrypted value needs no re-encryption: the ciphertext's associated data binds the settings scope, namespace and key, never the holding object or an organization.
26+
- **Authored references.** A filter, list-view column or seed that names `scope = 'global'` on `sys_setting` matches nothing: point it at `sys_platform_setting`, which has no `scope` column.
27+
- **Generic data reads of the global values.** Read `sys_platform_setting`; it needs `manage_platform_settings`. The settings door (`/api/settings/:namespace`) is unchanged and keeps applying each manifest's own read and write capability.
28+
- **Kernels that register the settings objects by hand.** Register `SysPlatformSetting` (`@objectstack/platform-objects/system`) beside `SysSetting`; `SettingsServicePlugin` already does. The service reads both on every resolution, so a kernel missing one fails the read loudly rather than answering a cascade with a rung missing.

‎.changeset/22080-v18-line-opens.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
The v18 line opens (ADR-0131; #22050). Every release from here is an `18.0.0-next.N` prerelease on the npm dist-tag `next`, until 18.0 GA.
6+
7+
Clause-②: no
8+
9+
- **What this marks.** Changesets is in pre mode with the tag `next`. Every `@objectstack/*` package versions together, so this one `major` takes all of them to 18, and the first version cut is `18.0.0-next.0`. `PROTOCOL_VERSION` follows `@objectstack/spec`'s major at version time, so it reads `18.0.0` from that prerelease on. The marker itself changes no code, key, export or stored shape.
10+
- **What the line carries.** ADR-0131 (organization ownership is total: no NULL `organization_id`) lands on this line in stages. Each stage that breaks something says so in its own changeset, with its own migration.
11+
- **Who receives it.** A plain install still resolves the dist-tag `latest`, which stays on 17.x until 18.0 GA. A prerelease is installed only on request: `@objectstack/spec@next`, or an exact `18.0.0-next.N`.
12+
- **What ends it.** 18.0 GA exits pre mode and publishes to `latest`.
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) the v18 line's opening marker: Changesets pre mode plus one major on the fixed group, so the train versions 18.0.0-next.N. No authorable key, spelling, export, type or stored shape moves in this diff, and no stored row is read, rewritten or converted; each breaking stage of the line states its own disposition in its own changeset. 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 no TypeScript declaration moves (not runtime-interface-only or type-surface-only). -->
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/service-storage': patch
3+
---
4+
5+
fix(service-storage): finishing an upload after switching active organization answers `409 RESOURCE_CONFLICT` naming the change, not a `500` that reads as a data-engine outage
6+
7+
Clause-②: no
8+
9+
`POST /api/v1/storage/upload/complete` and `POST /api/v1/storage/upload/chunked/:uploadId/complete` write by id under the caller's active organization, and that write is scoped to it. When the uploader's active organization is no longer the one the upload was started in, the write cannot reach the upload's row. Both doors used to answer that with `500 INTERNAL` and a message diagnosing a data-engine outage ("Restore the data engine …").
10+
11+
- **Now:** before writing anything, each door answers `409 RESOURCE_CONFLICT` in the standard error envelope. The message says the upload was started in a different organization than the active one, and that switching back finishes it. The server log carries one warning naming the upload, the organization it was started in and the active one.
12+
- **Unchanged:** which organization an upload belongs to (switching back finishes it there); a commit or completion from the starting organization; a real data-engine failure, which still answers `500` with its consequence and fix; the uploader-only rule, which runs first, so a caller who did not start the upload is still refused `403 PERMISSION_DENIED` and learns nothing about organizations; uploads stamped with no organization; routes with no session resolver; a store with no data engine wired; and the progress door.
13+
14+
What changes for you: when either door answers `409 RESOURCE_CONFLICT`, switch the active organization back to the one the upload was started in and retry the same call.

‎.changeset/pre.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"mode": "pre",
3+
"tag": "next"
4+
}

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **54 of the 234 sites are spelled that way**. A
125+
forwarding shim cannot, and **52 of the 234 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -150,8 +150,8 @@ now **0**: nothing on this surface threads a context that provably lacks the fla
150150

151151
**"No tenant context" counted sites it had not read.** An options argument the
152152
walker could not parse was folded into the same bucket as one it had read and
153-
found empty. That published **62 sites "carrying no tenant context at all"**
154-
when 8 said so and 54 were simply unread — an over-claim in the *alarming*
153+
found empty. That published **60 sites "carrying no tenant context at all"**
154+
when 8 said so and 52 were simply unread — an over-claim in the *alarming*
155155
direction, on the very figure this page tells other cards to cite. `carries` is
156156
now three-valued, and an unreadable argument can never contribute to the
157157
provable count.
@@ -228,10 +228,10 @@ cannot read, and they are neither in nor out.
228228
| …whose object name is chosen at run time | 80 |
229229
| …against an object with tenancy ENABLED | 153 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 172 |
231+
| threading a tenant context | 174 |
232232
| PROVABLY carrying none (options read, no context key) | **8** |
233233
| …of those, against a decidably tenancy-enabled object | **2** |
234-
| options argument UNREADABLE — may or may not carry one | 54 |
234+
| options argument UNREADABLE — may or may not carry one | 52 |
235235
| …of those, against a decidably tenancy-enabled object | 31 |
236236
| threading a decidably ELEVATED (`isSystem`) context | 123 |
237237
| threading a context that is decidably NOT elevated | 0 |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-08 at `7f9500afd`.
300+
Measured on 2026-10-08 at `0328884e5`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 615 |
304+
| tracked non-test sources scanned | 617 |
305305
| engine-shaped types recognised | 70 |
306-
| declared objects in the registry | 116 |
306+
| declared objects in the registry | 117 |
307307
| same-named calls subtracted as non-engine | 160 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3838
| Object name chosen at run time | 80 |
3939
| Against a tenancy-enabled object | 153 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 172 |
41+
| Threading a tenant context | 174 |
4242
| Provably carrying none | 8 |
4343
| …and decidably tenancy-enabled | 2 |
44-
| Options argument unreadable | 54 |
44+
| Options argument unreadable | 52 |
4545
| …and decidably tenancy-enabled | 31 |
4646
| Threading a decidably elevated context | 123 |
4747
| Threading a decidably non-elevated context | 0 |
@@ -90,13 +90,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-08 at `7f9500afd`.
93+
Measured on 2026-10-08 at `0328884e5`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 615 |
97+
| tracked non-test sources scanned | 617 |
9898
| engine-shaped types recognised | 70 |
99-
| declared objects in the registry | 116 |
99+
| declared objects in the registry | 117 |
100100
| same-named calls subtracted as non-engine | 160 |
101101

102102
## Every site
@@ -239,8 +239,8 @@ Measured on 2026-10-08 at `7f9500afd`.
239239
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_secret` | enabled | elevated | 1 |
240240
| `packages/services/service-settings/src/settings-service-plugin.ts` | `update` | `sys_secret` | enabled | elevated | 1 |
241241
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_setting_audit` | enabled | elevated | 1 |
242-
| `packages/services/service-settings/src/settings-service.ts` | `insert` | `this.objectName` | undecidable | options unreadable | 1 |
243-
| `packages/services/service-settings/src/settings-service.ts` | `update` | `this.objectName` | undecidable | options unreadable | 1 |
242+
| `packages/services/service-settings/src/settings-service.ts` | `insert` | `object` | undecidable | context, elevation undecidable | 1 |
243+
| `packages/services/service-settings/src/settings-service.ts` | `update` | `object` | undecidable | context, elevation undecidable | 1 |
244244
| `packages/services/service-storage/src/attachment-lifecycle.ts` | `update` | `sys_file` | enabled | elevated | 3 |
245245
| `packages/services/service-storage/src/backfill-file-references.ts` | `update` | `object` | undecidable | options unreadable | 1 |
246246
| `packages/services/service-storage/src/backfill-file-references.ts` | `update` | `object` | undecidable | elevated | 1 |

‎docs/qa/platform-checklist/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ ever matters more than PR independence, re-adding the one-line CI step
454454
v<current>` item (same discipline as changesets); the release sweep filter catches
455455
stragglers.
456456
4. **Periodic re-sweep** → [SWEEP.md](./SWEEP.md) is a runbook any AI session can
457-
execute on request ("run a coverage sweep") — five independent gap-hunt angles,
457+
execute on request ("run a coverage sweep") — six independent gap-hunt angles,
458458
dedupe, author, validate. The 2026-08 sweep it encodes found 3 stale waivers and
459459
~55 missing items; re-running it is how drift that slips past 1–3 gets caught.
460460

‎docs/qa/platform-checklist/SWEEP.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ kind, no new enum member), so it is found only here.
3131

3232
## What a sweep is
3333

34-
Five READ-ONLY gap-hunter agents, each enumerating the platform from a different angle
34+
Six READ-ONLY gap-hunter agents, each enumerating the platform from a different angle
3535
and diffing it against the current checklist. Different angles catch different miss
3636
classes — the 2026-08 sweep's finds (4 factually-stale waivers, the untested built-in
3737
apps, sharing rules, the ACTION_LOCATIONS matrix) each came from a different angle a
@@ -44,16 +44,17 @@ single reader would not have covered.
4444
| 3. Routes & runtime | ALL route ledgers (runtime, rest, service-*, auth) + non-ledgered mounts | routes reachable but semantically untested; dispatcher-vs-hono seams (#3361 class) |
4545
| 4. Built-in apps | packages/apps/{setup,studio,account} page by page | admin/user pages nobody walked; settings/session/org surfaces |
4646
| 5. Docs claims | content/docs/capabilities/*.mdx, release plans, showcase tours | promised capabilities with no item; docs advertising retired features (PD#10) |
47+
| 6. Surface kinds vs the one design system | objectui `apps/console/src/preview-gallery.tsx`; the `@object-ui/components` primitives a surface composes (Dialog, Table, Input, Checkbox, Button, the custom wrappers under `packages/components/src/custom/`); every widget that composes them into a surface kind — dialog, record picker, list, form, record page, dashboard, empty state, loading state | surfaces that pass their functional items but override the design system — a wrapper carrying a second border, a container killing the primitive's header rhythm, a fixed width unrelated to content, selection without the Checkbox primitive, split status bars, a confirm enabled at zero selected, navigational links inside a picker (the `RecordPickerDialog` class, 2026-10) |
4748

4849
## How to run it
4950

5051
1. **Read the current state first** — every `areas/*.json`, `coverage.json`, and this
5152
dir's README/RUNNER. The gap is only real if nothing already covers it.
52-
2. **Dispatch the five hunters in parallel**, READ-ONLY (they write no files). Each
53+
2. **Dispatch the six hunters in parallel**, READ-ONLY (they write no files). Each
5354
returns a structured gap table: `surface | evidence path | current coverage (item id
5455
or NONE/partial) | proposed item id | sketch | stock fixture?`. Give each hunter the
5556
list of already-resolved gaps so they don't re-report.
56-
3. **Dedupe** the five reports into one register (the 2026-08 sweep used a
57+
3. **Dedupe** the six reports into one register (the 2026-08 sweep used a
5758
`PENDING-GAPS.md` scratch file, since deleted). Overlap is expected and is signal —
5859
a gap found from three angles is high-priority.
5960
4. **Author** the new items via per-area writer agents, one area file per writer so

‎packages/cli/src/commands/secret/rewrap.guards.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ function wireBoot(
108108
if (object === 'sys_setting') {
109109
return { async find() { harness.reads.push('sys_setting'); return rows.settings.map((r) => ({ ...r })); } };
110110
}
111+
// [ADR-0131 D7] The settings family's second holder: the global rung's store.
112+
if (object === 'sys_platform_setting') {
113+
return { async find() { harness.reads.push('sys_platform_setting'); return []; } };
114+
}
111115
if (object === 'sys_metadata') return { async find() { harness.reads.push('sys_metadata'); return []; } };
112116
return undefined;
113117
},
@@ -247,7 +251,7 @@ describe('os secret rewrap — guards that stop a run before any row is opened o
247251
}, 60_000);
248252

249253
it('a dry run over tables the boot measured absent reads none of them, and reports empty work', async () => {
250-
const h = wireBoot(freshRows(), { absent: ['sys_secret', 'sys_setting', 'sys_metadata'] });
254+
const h = wireBoot(freshRows(), { absent: ['sys_secret', 'sys_setting', 'sys_platform_setting', 'sys_metadata'] });
251255
const { payload, exitCode } = await run(['--no-declared-datasources']);
252256

253257
// "Not asked": a table that does not exist holds nothing, so no read is issued.

0 commit comments

Comments
 (0)