Skip to content

Commit 6db3704

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-7r0p6j
2 parents a63b2a0 + 41dc302 commit 6db3704

116 files changed

Lines changed: 5155 additions & 397 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/service-messaging': patch
3+
---
4+
5+
feat(service-messaging): serve the notification topics and channels a preference or template can name
6+
7+
Clause-②: yes (widening) — a new read-only route, `GET /api/v1/messaging/preference-options`, mounted by `MessagingServicePlugin` on the kernel's HTTP server; nothing existing is narrowed or refused.
8+
Metadata-protocol: none
9+
10+
The topic and channel of a notification preference stay free text, `*` is still stored as written, and nothing new is refused. What is new is a list the console can offer beside the text box, so an administrator picks a value instead of typing a machine name from memory.
11+
12+
- **The route.** `GET /api/v1/messaging/preference-options` answers `{ topics, channels }`. `topics` starts with `*` and lists the platform's fixed topics (`approval.requested`, `collab.mention`, …), the topics this environment's flow `notify` steps send under (`notify` for a step that names none), and the topics already stored on this organization's notifications, each once with every source that names it. `channels` starts with `*` and lists every channel the service knows, with `configured: false` on one whose service is not set up yet (email with no email service, …). Any signed-in member may read it, as on `/api/v1/notifications`; an anonymous caller is refused `401`, a held sign-in `403`, and a query parameter `400`. A topic stored in one organization is never listed for another.
13+
- **The fields.** `sys_notification_preference.topic` and `sys_notification_template.topic` declare `widget: 'notification-topic'`, and `sys_notification_preference.channel` declares `widget: 'notification-channel'`, for the console's pick-or-type inputs; until the console registers them, the fields render as the same text boxes. The template's channel keeps its existing refusal.
14+
- **The help.** The preference channel's help now names `push`, available once a push service is configured, in every language.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the `objectstack-ai` skill's MCP tools table now teaches that `delete_record` needs `confirm: true` — a call without it is refused with `ACTION_CONFIRMATION_REQUIRED` (428), so the agent asks the human first, then retries. The row said only that `delete_record` is annotated destructive, so an agent following it was refused since the MCP tool started requiring confirmation.
6+
7+
Clause-②: no
8+
Metadata-protocol: none
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/console': patch
3+
---
4+
5+
fix(console): the "Download skill" SKILL.md teaches `delete_record` with `confirm: true`, and the rest of what the server serves
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The Integrations page's "Download skill" button wrote a SKILL.md that taught `delete_record({ objectName, recordId })` with no `confirm`. The MCP server refuses that call with `ACTION_CONFIRMATION_REQUIRED` and deletes nothing, so an agent that followed the downloaded skill failed every delete. The download now carries the same text the MCP server serves to an API key or a session: `delete_record({ objectName, recordId, confirm })` with the confirmation rule, the business-action tools (`list_actions`, `run_action`, `resume_run`), `validate_expression`, the OAuth connection track, and the `Authorization: Bearer` key form. A test compares the download with the server's renderer byte for byte, so the two cannot drift apart again unnoticed.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/plugin-security': minor
5+
'@objectstack/objectql': minor
6+
---
7+
8+
feat(spec, security): a `controlled_by_parent` object may declare an optional master, and a detail row with no master is read by its owner
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: added
12+
Skills-docs: follow-up owed — #30907 (card 3/3 of the ruling: the docs and skills that teach `controlled_by_parent` still say the master is always required); this diff teaches the optional master in ADR-0055 §1 and §2 as amended, in `os explain relationship/master-detail-required` and `os explain security-controlled-by-parent-no-relation`, and in the semantic migration entry `cbp-optional-master-admitted`
13+
14+
A rule like "a rep sees the contacts of the accounts they own; a contact need not have an account" now has a declarative form. Declare the detail's only `master_detail` with `required: false`:
15+
16+
```ts
17+
fields: {
18+
account: Field.masterDetail('crm_account', { label: 'Account', required: false }),
19+
},
20+
sharingModel: 'controlled_by_parent',
21+
```
22+
23+
- **`@objectstack/spec`: the optional master.** A `controlled_by_parent` object whose only `master_detail` is declared `required: false` passes the parse and `ObjectSchema.create()` with that value kept. `controlledByParentMasterOf(fields)` is exported from `@objectstack/spec/data` beside `controlledByParentMasterRefusal`: it answers the one master's field and whether it is optional. `isControlledByParentMasterField` still means the required master. Every other refusal stands: two masters, a required `lookup` only, and a `readonly` or `system` master. `ObjectSchema.create()` still forces an omitted `required` to `true`, and still refuses `required: false` on a `master_detail` beside another one. A stored row the v18 upgrade listed because its master declared `required: false` is valid now and no longer listed (`os migrate meta`); the semantic entry `cbp-optional-master-admitted` records it. The raw parse reads an omitted `required` as `false` and so admits it as the optional master; `os lint` reports the omission (below).
24+
- **`@objectstack/plugin-security`: the null-master read arm.** For an optional master, the controlled-by-parent read filter is the unchanged master arm OR a second arm for rows whose master reference is empty: every such row for a holder of View All on the detail, else the caller's own (`owner_id`), else none (an object without an `owner_id` column). A row with no owner (a queue row) is View All's alone. The two arms never share a row, and View All on the detail does not open the master arm. Every read door composes the one filter, so the data API, analytics, search, export, the parent gates of attachments, comments, activity and approvals, chains and delegated reads follow. A required master's filter is unchanged. `security/explain` names the null-master arm on a record read. Writes are unchanged: an insert or update that leaves the master reference empty is still refused with 422.
25+
- **`@objectstack/lint`.** `relationship/master-detail-required` passes the optional master, still reports `readonly` / `system` on it, and reports an omitted `required` on the only `master_detail` at `error`, naming both spellings (`required: true`, or `required: false` for an optional master). `security-controlled-by-parent-no-relation` and `-ambiguous-relation` resolve the optional master as the parse does.
26+
- **`@objectstack/objectql`.** The boot refusal of a master the engine cannot compose a read over now judges an optional master's detail too.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the `fieldPatches` example relabels HotCRM's `industry` and patches options only on a field that declares its own
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`). The added bytes are paid by deleting a restatement in the same file: the `.custom` paragraph's closing clause "a base field changes only through `fieldPatches`", whose facts the relabel paragraph above it already states. No token ceiling moves. No schema, key or behaviour changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `fieldPatches` description's example no longer patches an option of a picklist-bound field
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`ObjectExtensionSchema.fieldPatches`'s description and the `ObjectExtensionSchema` TSDoc taught `industry: { label: 'Sector', options: { technology: { label: 'Tech' } } }`. HotCRM's `crm_account.industry` takes its options from a shared picklist, so the install door and `os validate` refuse that patch (`picklist-option`). The description now says an option of a picklist-bound field is never patchable and only a field declaring its own `options` takes an option patch, and its example sets `industry`'s label and help text and patches `type`'s `customer` option. The generated `references/data/object.mdx` follows. Description text only: no schema, key or behaviour changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
feat(plugin-auth): a platform administrator can mint and revoke SCIM connection credentials
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
Skills-docs: updated here — `content/docs/permissions/authentication.mdx` ("SCIM Connection Credentials" under Admin User Management); `skills/**` teaches no SCIM setup
10+
11+
An identity provider (Okta, Entra ID) provisions users through `/api/v1/auth/scim/v2/*` with a bearer credential that this platform issues, but nothing could issue one: the mint existed with no caller, and the credential object refuses create on the data API, so no identity provider could connect. Two routes now do it, for the platform administrator only (the shared ADR-0068 gate: anyone else is refused `403 PERMISSION_DENIED`, `401 UNAUTHENTICATED` when signed out):
12+
13+
- `POST /api/v1/auth/admin/scim/mint-credential` with `{ connectionId, label?, expiresAt?, provisioningDomainId?, organizationId? }` answers `{ credentialId, connectionId, token, expiresAt }`. `connectionId` is at most 246 characters, so the connection's audit actor `svc:scim:<connectionId>` fits `sys_audit_log.actor`. The `token` is shown once, in that response only (`Cache-Control: no-store`); only its keyed one-way digest is stored, and it is not readable through the data API, the audit log or the logs. Refused `503 SERVICE_UNAVAILABLE` while SCIM is off. The credential row is owned by `organizationId` when given (it must exist), otherwise by the Default Organization.
14+
- `POST /api/v1/auth/admin/scim/revoke-credential` with `{ credentialId }` switches the credential off, so the identity provider's next SCIM request with it answers `401`. `404 RESOURCE_NOT_FOUND` for an unknown credential, `409 RESOURCE_CONFLICT` for one already revoked. Served whether or not SCIM is on.
15+
16+
Rotation is a second mint for the same `connectionId`, moving the identity provider to the new bearer, then revoking the old one. Each mint and each revoke writes one `sys_audit_log` row naming the administrator and the connection, never the credential. Nothing that existed changes: no route, response or stored row moves, and the credential object stays read-only on the data API.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
feat(runtime): a host can supply where module enable/disable state lives — `createPackageStateStorePlugin(store)` — and a store that refuses a write makes the enable or disable door fail with nothing changed
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
10+
Package lifecycle state (which packages an operator has disabled) has lived only in a file under `OS_HOME`, which a hosted container loses when it is replaced. `@objectstack/runtime` now exports the seam a host uses to keep it elsewhere:
11+
12+
- **`PackageStateStore`**: `loadDisabledPackageIds(environmentId)` and `setPackageDisabled(environmentId, packageId, disabled)`, either may return a promise.
13+
- **`createPackageStateStorePlugin(store)`** (and the `PackageStateStorePlugin` class it builds): add it to the kernel's plugins. It registers the store as the `PACKAGE_STATE_STORE_SERVICE` (`'package-state-store'`) service under the plugin name `PACKAGE_STATE_STORE_PLUGIN_NAME`, which `AppPlugin` lists in its `optionalDependencies`, so the store is in place before the boot seed reads it whatever slot it is composed in. A store missing either method is refused with a `TypeError`.
14+
15+
With a host store composed, the boot seed reads it instead of the file, and the package doors (install, `PATCH /packages/:id/enable`, `PATCH /packages/:id/disable`, `DELETE /packages/:id`) write to it; the file under `OS_HOME` is neither read nor written. A host store that throws:
16+
17+
- on a write from the enable or disable door: the door changes nothing in memory and answers `503 SERVICE_UNAVAILABLE`, saying the switch did not happen and can be retried. The doors now write the store before they switch the package.
18+
- on a write from install or delete, whose change has already landed: logged at `error`, naming what the next boot will read.
19+
- on the boot read: the boot fails, naming the store, rather than bring every disabled package back enabled.
20+
21+
A store registered under the service name by a plugin of the host's own, which nothing orders ahead of `AppPlugin`, fails `AppPlugin.start()` with the fix (use `createPackageStateStorePlugin`), instead of serving a boot seeded from the file while the doors write to the store.
22+
23+
Without a host store nothing changes: the same file, the same bytes, and a failed file write still only warns.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(app-shell): the Setup packages page shows a disable refused for a dependent validation rule whole, and its confirm dialog says such a refusal can happen
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
When `PATCH /api/v1/packages/:id/disable` answers `409 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE` (an active validation rule of an enabled package reads a field the package adds through `objectExtensions`), the package sheet now draws a heading in the reader's language over the server's whole sentence: every blocking rule as package · object · rule, then the remedy. Before, the message was cut to its first sentence and the remedy sat behind "More". Every other refusal is drawn as before. The Disable confirm dialog gains one line saying that such a rule can still refuse the disable, because the served `disableable` verdict says what the package allows and does not read rules elsewhere. Two new keys in the `@object-ui/i18n` `studio` namespace (`en` and `zh`): `engine.packages.detail.disableRestricted` and `engine.packages.detail.disableMayBeRefused`.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
fix(service-datasource)!: the datasource admin door asks the walled-catalog verdict on every row-writing verb, as the /meta door does
6+
7+
Clause-②: no (narrowing) — under the `isolated` and `group` tenancy postures, creating, changing or removing a datasource at the datasource admin routes (`POST /api/v1/datasources`, `PATCH` and `DELETE /api/v1/datasources/:name`, `POST /api/v1/datasources/:name/migrate-credential`) is a write to the environment's metadata catalog, and it is the platform operator's, as every other write to that catalog is.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and nothing at rest changes: which caller the datasource admin routes admit to a datasource write under a walled tenancy posture is all that moves, and every stored datasource stays as it is. -->
11+
12+
**BREAKING** accept-set narrowing at the datasource admin routes (`@objectstack/service-datasource`) under a walled tenancy posture.
13+
14+
- **The rule.** Under `isolated` and `group` every organization shares the environment's metadata catalog, and a write to it is admitted only for the platform operator and for the engine's own writes (ADR-0131 D3, D12 items 7–8, D11). A datasource is a `datasource` item of that catalog, the same record `PUT /api/v1/meta/datasource/:name` writes. So each datasource admin route that writes a datasource record (create, update, remove, migrate-credential) asks the walled-catalog verdict the `/meta` write doors ask (`walledCatalogWriteVerdict`, `@objectstack/metadata-core`) for the type `datasource`, after the `manage_platform_settings` capability gate and before the datasource service is looked up.
15+
- **The answer.** A refused call answers `403 PERMISSION_DENIED` with the verdict's sentence for a `datasource` item, the one the `/meta` write doors answer, naming the posture. Nothing is stored, no secret is sealed or removed, and no pool is rebuilt or evicted.
16+
- **Unchanged.** The platform operator writes datasources under a wall as before. Under `single`, and on a deployment with no `tenancy` service, every caller holding `manage_platform_settings` writes datasources as before. Listing, reading, the driver catalog, both connection tests, the remote-table listing and the object draft are unchanged, and the capability gate answers first, as before.
17+
- **Who moves.** On a walled deployment, a datasource create, update, removal or credential migration by a caller below the platform operator, an organization's `admin_full_access` holder included, is refused; the platform operator performs it.

0 commit comments

Comments
 (0)