Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .changeset/30505-skills-declared-action-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/skills': patch
---

docs(skills): the platform skill teaches the declared action override and its access-widening refusal

Clause-②: no
Metadata-protocol: none

- **Before.** `objectstack-platform`'s customization paragraph (ADR-0146) listed the types a `.custom` pair's declared override is honoured for without `action`, and `content/docs/concepts/metadata-lifecycle.mdx` said an `action` override was not served yet. Both have been false since `action` opened (ADR-0146 D9).
- **Now.** The skill lists `action` and says what an action override may change: it keeps the base's `objectName` (the doors match the base's action as `OBJECT:NAME`, and moving it to another object is refused `declared_package_ships_no_such_item`), and it replaces the behaviour (`type`, `body`, `label`, `visible`, …) whole for whoever could already run the base. Dropping a `requiredPermissions` entry, turning `ai.exposed` on or `ai.requiresConfirmation` off is refused `403 NOT_OVERRIDABLE` with rule `declared_action_widens_access`. The docs page says the same, and the skill's evals gain a case for it.
- No schema, key or behaviour changes.
17 changes: 13 additions & 4 deletions content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,21 @@ document **whole** for every read that serves the base's name
override, `effective` = the override), the list scoped to the base and the
environment-wide list all serve the override, stamped `_packageId: 'hotcrm'`. The
declaration counts only from the pair — the stored row's own `package_id` must be
`hotcrm.custom` — and for every type of `OVERRIDABLE_METADATA_TYPES` (`@objectstack/spec/kernel`)
but `action`: a `view`, `dashboard`, `report`, `email_template`, `translation`, `flow`,
`page`, `app`, `dataset`, `doc`, `book` or `mapping`. An `action` override waits for
ADR-0146 D9's conditions and is not served yet. A declaration from any other package is ignored.
`hotcrm.custom` — and for every type of `OVERRIDABLE_METADATA_TYPES` (`@objectstack/spec/kernel`):
a `view`, `dashboard`, `report`, `email_template`, `translation`, `flow`, `action`,
`page`, `app`, `dataset`, `doc`, `book` or `mapping`. A declaration from any other package is ignored.
The import door and the connector pull read a named `mapping` through the by-name read,
so they run the pair's mapping.
- **An `action` override changes what runs, never who may run it** (ADR-0146 D9). It is served
wherever the action is drawn or run (the object read's `actions`, the run doors) and replaces
the base's behaviour (`type`, `target`, `body`, `params`, `label`, `visible`, …) whole, for the
people who could already run the base's action. It keeps the base's `objectName`: every door
matches the base's action as `OBJECT:NAME`, so an override that moves it to another object is
refused `403 NOT_OVERRIDABLE` with rule `declared_package_ships_no_such_item`. An override that
drops a capability from the base's `requiredPermissions`, sets `ai.exposed: true` on an action
the base keeps off the agent surface, or drops the base's `ai.requiresConfirmation: true` is
refused `403 NOT_OVERRIDABLE` with rule `declared_action_widens_access`; adding a capability
is allowed.
- **Removing the override brings the base's back.** `DELETE /meta/:type/:name?package=hotcrm.custom`
("Restore package original") deletes the pair's row, for every type above, and every read
then serves the base's document again. A `DELETE` naming `hotcrm` removes nothing of the pair's.
Expand Down
37 changes: 12 additions & 25 deletions skills/objectstack-platform/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ metadata:

## `defineStack()` — The Core Configuration

→ Moved verbatim to [references/bootstrap.md](./references/bootstrap.md) § Minimal Example.
→ See [references/bootstrap.md](./references/bootstrap.md) § Minimal Example.

### Full Configuration Reference

Expand Down Expand Up @@ -62,13 +62,12 @@ Undeclared keys are handled per surface, in two postures:
the surface and the offending key. TypeScript rejects the literal earlier
still, with `TS2353: Object literal may only specify known properties`.
- **Warned, then dropped** — the authoring surfaces whose shapes have not
been closed yet (`connectors` is one). `defineStack()` prints the warning
before the parse, and the value does not survive it:
been closed yet (`connectors` is one). `defineStack()` warns before the parse:
`defineStack: connectors.stripe.bogusKey: 'bogusKey' is not a declared connector key, so its value is dropped at load.`
Treat these as errors-in-waiting: a key that only warns today is expected
to be refused later.

→ Moved verbatim to [references/bootstrap.md](./references/bootstrap.md) § Map Format (Key → Name) · § Barrel Import Pattern.
→ See [references/bootstrap.md](./references/bootstrap.md) § Map Format (Key → Name) · § Barrel Import Pattern.

### Strict Validation

Expand All @@ -78,11 +77,7 @@ Undeclared keys are handled per surface, in two postures:
2. **Cross-references** — views/actions/flows reference defined objects
3. **Seed data** — dataset objects exist in the definition

To disable (e.g., objects provided by another plugin):

```typescript
export default defineStack(config, { strict: false });
```
Disable it with `defineStack(config, { strict: false })` (objects another plugin provides).

### Compile Artifact and Runtime Metadata Boundary

Expand Down Expand Up @@ -116,7 +111,7 @@ manifest: {
```

**`manifest.engines.protocol`:** the metadata-protocol major the app is authored
against (stamped by `create-objectstack`; the example apps carry it). The runtime
against (stamped by `create-objectstack`). The runtime
checks it **before it loads anything** and refuses an out-of-range app at the
boundary with the exact migration command. Change it when you deliberately move
to a new protocol major — never to silence a mismatch.
Expand All @@ -125,9 +120,9 @@ to a new protocol major — never to silence a mismatch.

**`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `<ns>__<short>` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). `os lint` additionally emits a non-fatal `naming/namespace-prefix` warning for bare-named UI/automation items (app, page, dashboard, flow, action, report, dataset) when a namespace is set.

**Customizing an installed package (ADR-0146):** Customize an installed base `hotcrm` in its paired writable package `hotcrm.custom`, which depends on it: a same-named item there with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (its `manifest.id`/`manifest.version`) replaces the base's document whole. The read honours it only from the pair (the row's `package_id`), for a view, dashboard, report, email_template, translation, flow, page, app, dataset, doc, book or mapping, and never judges `basedOn`. An email template is one item per (name, locale): an override names a locale the base ships; a locale the base does not ship is added without `overrides`. Then no `?package`, `?package=hotcrm`, the layered read and both lists serve it, and removing the pair's item (`DELETE …?package=hotcrm.custom`) serves the base's again; the env-wide list holds one item per name one code package ships (a dependent's stays under its own `?package=`); a pre-D11 unbound Setup row wins over the base's until `os migrate packageless-overlays` moves it, and once movable as a declared override, its package-less save, publish, reset and rollback are refused (D11); an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The base's address serves the pair's added locale too (both lists, the layered read with `code` null and `effective` the pair's, the execution list, a send in that locale); a locale a dependent adds under the base's name is served only under its own `?package=`. An undeclared pair row or a dependent's row at a locale the base ships is an orphan in the layered read's `overlay`, `effective` = the base's. An override may narrow a public form's anonymous intake, never re-open one any honoured body withdrew. `GET /packages/hotcrm.custom/export` carries it with `overrides`.
**Customizing an installed package (ADR-0146):** Customize an installed base `hotcrm` in its paired writable `hotcrm.custom`, which depends on it: a same-named item there with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` (its `manifest.id`/`manifest.version`) replaces the base's document whole. Reads honour it only from the pair (the row's `package_id`), for a view, dashboard, report, email_template, translation, flow, action, page, app, dataset, doc, book or mapping, never judging `basedOn`. An email template is one item per (name, locale): an override names a locale the base ships; one it does not ship is added without `overrides`. Then no `?package`, `?package=hotcrm`, the layered read and both lists serve it, and removing the pair's item (`DELETE …?package=hotcrm.custom`) serves the base's again; the env-wide list holds one item per name one code package ships (a dependent's stays under its own `?package=`); a pre-D11 unbound Setup row wins over the base's until `os migrate packageless-overlays` moves it, and once movable as a declared override, its package-less save, publish, reset and rollback are refused (D11); an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The base's address serves the pair's added locale too (both lists, the layered read with `code` null and `effective` the pair's, the execution list, a send in that locale); one a dependent adds under the base's name is served only under its own `?package=`. An undeclared pair row or a dependent's row at a base-shipped locale is an orphan in the layered read's `overlay`, `effective` = the base's. An override may narrow a public form's anonymous intake, never re-open one any honoured body withdrew. `GET /packages/hotcrm.custom/export` carries it with `overrides`.

The pair comes from `POST /api/v1/packages/hotcrm/ensure-customization`: `namespace` = the base's + `_custom`, `dependencies: { hotcrm: '^<major>' }`; `GET /packages/hotcrm/customizations` lists its overrides, extensions and switched-off items, and uninstalling the base is refused (`409`) while the pair exists. Only the pair declares: the save, publish and install doors, `os validate` and `os build` refuse `overrides` from any other package, and a dependent's item under a base item's name — its one fix is a new name. A permission set, position or capability is never overridden: copy it under a new name ("new similar", landing in the pair). An object is extended (`__c` fields, **objectstack-data**); a flow is overridden or copied (**objectstack-automation**).
The pair comes from `POST /api/v1/packages/hotcrm/ensure-customization`: `namespace` = the base's + `_custom`, `dependencies: { hotcrm: '^<major>' }`; `GET /packages/hotcrm/customizations` lists its overrides, extensions and switched-off items, and uninstalling the base is refused (`409`) while the pair exists. Only the pair declares: the save, publish and install doors, `os validate` and `os build` refuse `overrides` from any other package, and a dependent's item under a base item's name — its one fix is a new name. A permission set, position or capability is never overridden: copy it under a new name ("new similar", landing in the pair). An object is extended (`__c` fields, **objectstack-data**); a flow is overridden or copied (**objectstack-automation**). An action override keeps the base's `objectName` (keyed `OBJECT:NAME`; another object is refused `declared_package_ships_no_such_item`) and replaces its behaviour (`type`, `body`, `label`, `visible`, …) whole, for whoever could already run the base: dropping a `requiredPermissions` entry, turning `ai.exposed` on or `ai.requiresConfirmation` off is refused `declared_action_widens_access`, both `403 NOT_OVERRIDABLE`.

That read's `contributions` lists what other packages add to the base, one entry per item with its `packageId`: a `field` or `flow` names the base's `object` it is on, a `nav` item the base's `app`, its `group` if any, and its id as `name`; `counts.contributions` counts items, not packages.

Expand Down Expand Up @@ -175,7 +170,7 @@ what the platform owns.
The five retired remote templates (`todo`, `compliance`, `content`,
`contracts`, `procurement`) are refused by name.

→ Moved verbatim to [references/bootstrap.md](./references/bootstrap.md) § Scaffolding Command.
→ See [references/bootstrap.md](./references/bootstrap.md) § Scaffolding Command.

---

Expand Down Expand Up @@ -352,19 +347,15 @@ Five rules that change what you write:
`AppPlugin` invokes it during boot and hands the app live runtime handles: this
is the one seam where declarative metadata reaches imperative code (registering
action handlers, giving a job its data handle, provisioning a fixture
datasource). All three example apps use it.
datasource).

```typescript
export const onEnable = async (ctx: { ql: { registerAction: (...a: unknown[]) => void } }) => {
registerTaskActionHandlers(ctx.ql);
};
```

→ Moved verbatim to [references/bootstrap.md](./references/bootstrap.md) § Plugin Loading Order Matters · § Programmatic Bootstrap (Without CLI).

---

→ Moved verbatim to [references/bootstrap.md](./references/bootstrap.md) § Multi-App Composition.
→ See [references/bootstrap.md](./references/bootstrap.md) § Plugin Loading Order Matters · § Programmatic Bootstrap (Without CLI) · § Multi-App Composition · § Complete Working Example.

---

Expand Down Expand Up @@ -405,15 +396,11 @@ existing account on the boot banner.

---

→ Moved verbatim to [references/bootstrap.md](./references/bootstrap.md) § Complete Working Example.

---

→ Part 2 — Plugin Development & Kernel Extension moved verbatim to [references/plugin-development.md](./references/plugin-development.md) (Quick Reference — Detailed Rules · ObjectKernel vs LiteKernel · Plugin Interface — Quick Overview · PluginContext API · Complete Plugin Example · Using Plugins · Testing Plugins · Well-Known Plugin Names & Services · MetadataPlugin Runtime Boundary · Health Monitoring · Feature Flags); its detailed rules stay at [rules/plugin-lifecycle.md](./rules/plugin-lifecycle.md) · [rules/service-registry.md](./rules/service-registry.md) · [references/plugin-hooks.md](./references/plugin-hooks.md).
→ Part 2 — Plugin Development & Kernel Extension: [references/plugin-development.md](./references/plugin-development.md) (Quick Reference — Detailed Rules · ObjectKernel vs LiteKernel · Plugin Interface — Quick Overview · PluginContext API · Complete Plugin Example · Using Plugins · Testing Plugins · Well-Known Plugin Names & Services · MetadataPlugin Runtime Boundary · Health Monitoring · Feature Flags); its detailed rules stay at [rules/plugin-lifecycle.md](./rules/plugin-lifecycle.md) · [rules/service-registry.md](./rules/service-registry.md) · [references/plugin-hooks.md](./references/plugin-hooks.md).

---

→ Part 3 — Operations: CLI, Testing, Deployment moved verbatim to [references/operations.md](./references/operations.md) (Daily-loop commands · Build & runtime · Verify your work · Ports & networking · Data & migrations · Environments & deploy · Shipping a plugin · Testing pattern · Deployment targets · Health & observability · Common ops pitfalls).
→ Part 3 — Operations: CLI, Testing, Deployment: [references/operations.md](./references/operations.md) (Daily-loop commands · Build & runtime · Verify your work · Ports & networking · Data & migrations · Environments & deploy · Shipping a plugin · Testing pattern · Deployment targets · Health & observability · Common ops pitfalls).

---

Expand Down
20 changes: 19 additions & 1 deletion skills/objectstack-platform/evals/config-plugins-ops.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@
{
"id": 13,
"prompt": "HotCRM (`hotcrm`, 3.2.0) ships the `crm_pipeline` dataset and the `crm_sales_overview` page. Can we replace both with our own the way we replaced its `crm_account_grid` view, or is that only for views?",
"expected_output": "The same declaration, both types. In `hotcrm.custom`, an item of the same name and type with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` in its body replaces the base's dataset or page whole, and every read of the name serves it: a pair's declared override is served for a view, dashboard, report, email_template, translation, flow, page, app, dataset, doc, book or mapping. Not an edit of HotCRM's read-only item, not a package-unbound Setup row, and not an object extension (`__c` fields add to an object and replace no dataset or page).",
"expected_output": "The same declaration, both types. In `hotcrm.custom`, an item of the same name and type with `overrides: { package: 'hotcrm', basedOn: '3.2.0' }` in its body replaces the base's dataset or page whole, and every read of the name serves it: a pair's declared override is served for a view, dashboard, report, email_template, translation, flow, action, page, app, dataset, doc, book or mapping. Not an edit of HotCRM's read-only item, not a package-unbound Setup row, and not an object extension (`__c` fields add to an object and replace no dataset or page).",
"files": [],
"assertions": {
"must_contain": [
Expand Down Expand Up @@ -249,6 +249,24 @@
"PUT /api/v1/meta/view/crm_account_grid"
]
}
},
{
"id": 15,
"prompt": "HotCRM (`hotcrm`, 3.2.0) ships the `crm_close_deal` action on `crm_opportunity` with `requiredPermissions: ['crm_close_deal']`. The customer wants it relabelled \"Mark won\", run a different script, and usable by every sales rep, including reps without that capability. How do we change it?",
"expected_output": "Relabel it and change what it runs with a declared override in the paired `hotcrm.custom`: an action of the same name on the same object (`objectName: 'crm_opportunity'`) carrying `overrides: { package: 'hotcrm', basedOn: '3.2.0' }`. Its behaviour (`label`, `type`, `body`, …) replaces the base's whole, and it runs for whoever could already run the base action. It cannot widen who that is: dropping `crm_close_deal` from `requiredPermissions` is refused `403 NOT_OVERRIDABLE` (`declared_action_widens_access`), and so is turning `ai.exposed` on or `ai.requiresConfirmation` off. Give the reps the capability through their permission set instead. Moving the action to another object is refused too (`declared_package_ships_no_such_item`). Not an edit of HotCRM's read-only action and not a copy under the same name from a dependent package.",
"files": [],
"assertions": {
"must_contain": [
"hotcrm.custom",
"overrides:",
"requiredPermissions",
"declared_action_widens_access"
],
"must_not_contain": [
"requiredPermissions: []",
"PUT /api/v1/meta/action/crm_close_deal"
]
}
}
]
}
Loading