You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 6a983a1
Browse filesBrowse the repository at this point in the historyBrowse files
docs(skills): teach the declared action override and its access-widening refusal (#30704)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DMSEjCoKbRvbJ1FTH8sTzoJ)_
domain:skills#1 · os-elon-musk · session_016WV4czCxwMepBDxUtozyne
Fixes#30505
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`objectstack-platform` 技能在 ADR-0146 定制段把 `action`
列入可声明覆盖的类型,并补一句讲清 action 覆盖能改什么、不能改什么;`metadata-lifecycle.mdx` 删掉"action
覆盖尚未提供"的过时句,改写为同样的规则;技能评估加一条 action 覆盖用例。
- 为什么改:PR #30497(落地 ADR-0146 D9)与 PR #30612 之后,action
覆盖已经可用,但技能和文档仍说不行,AI 会写错或绕路克隆。
- 风险与回滚:只有教学文字,无代码、无 schema 变化;回滚即 revert 本 PR。token 净增 1(6009→6010,上限
6011),靠同文件压缩付账,未动上限。
- 先看:`skills/objectstack-platform/SKILL.md` 定制段新增的 action
句与压缩处;`content/docs/concepts/metadata-lifecycle.mdx` 第 130–146
行;`skills/objectstack-platform/evals/config-plugins-ops.json` 用例 15。
- 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期看到
`objectstack-platform/SKILL.md is 6010 tokens (ceiling 6011; headroom
1)`。
- 需要你做的:受管面(`skills/**`,Tier H),需治理审批人批准后由席位落地。
## What changes
- **`skills/objectstack-platform/SKILL.md`** (the ADR-0146 customization
paragraphs): `action` joins the list of types a pair's declared override
is honoured for, and one sentence teaches what an action override may
change. It keeps the base's `objectName` (the doors match the base's
action as `OBJECT:NAME`; an override on another object is refused
`declared_package_ships_no_such_item`) and replaces the behaviour
(`type`, `body`, `label`, `visible`, …) whole, for whoever could already
run the base. Dropping a `requiredPermissions` entry, turning
`ai.exposed` on, or turning `ai.requiresConfirmation` off is refused
`403 NOT_OVERRIDABLE` with rule `declared_action_widens_access`.
- **`content/docs/concepts/metadata-lifecycle.mdx`**: the sentence
saying an `action` override waits for ADR-0146 D9 and is not served is
gone; `action` is in the type list, and a new bullet states the same
rules in full prose.
- **`skills/objectstack-platform/evals/config-plugins-ops.json`**: new
case 15 (relabel and re-script a packaged action, and a request to widen
who may run it); case 13's type list gains `action`. The platform evals
carry one case per taught override rule (cases 12–14), so the new rule
gets one too.
- **`.changeset/30505-skills-declared-action-override.md`**:
`'@objectstack/skills': patch`.
Every fact was read off `origin/main` `b77c160a`, not off the card:
`OVERRIDE_TYPE_CLASSIFICATION.action` (`disposition: 'overridable'`) in
`packages/spec/src/kernel/customization-package.ts`;
`actionOverrideWidening` and the `declared_action_widens_access` /
`declared_package_ships_no_such_item` refusals (`403`,
`NOT_OVERRIDABLE`) plus the `OBJECT:NAME` shipper filter in
`packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`. The
confirmation key is `ai.requiresConfirmation`, nested under `ai` like
`ai.exposed`, so the skill spells both that way.
## Why
PR #30497 opened the declared override for `action` (ADR-0146 D9), and
PR #30612 added the `objectName` rule. The skill still listed the
overridable types without `action`, and the docs page said an action
override was not served. An agent reading either would refuse or clone a
package's button instead of overriding it.
## Token ratchet (`skills/**`)
| Reading | Before (`b77c160a`) | After (`17e0285f`) |
|:--|--:|--:|
| `objectstack-platform/SKILL.md` lines | 426 | 413 |
| `objectstack-platform/SKILL.md` tokens (ratchet count, ceiling 6011) |
6009 | 6010 |
| All 11 `SKILL.md` files, lines | 4534 | 4521 |
| All 11 `SKILL.md` files, tokens | 56667 | 56668 |
The addition (~100 tokens) is paid in the same file, with no ceiling
raise. Each cut removes words, never a re-wrap:
- The customization paragraph is tightened ("one it does not ship", "a
base-shipped locale", "never judging `basedOn`").
- The `strict: false` fence becomes one inline sentence.
- The four "Moved verbatim to" pointers read "See", and the Part 2 /
Part 3 pointers drop "moved verbatim to".
- The two single-section `bootstrap.md` pointer stubs (§ Multi-App
Composition, § Complete Working Example) merge into the pointer above
them, so one line names all four sections.
- Two observations are dropped: "the example apps carry it" (engines
stamp) and "All three example apps use it" (`onEnable`). The
`defineStack()` warn sentence is shortened; the example warning text it
introduces already says the value is dropped.
## Acceptance notes
- `content/docs/concepts/metadata-lifecycle.mdx` L208 (ADR-0126 table)
still names only the switch-off route for a packaged action. That route
still exists (`packaged-base-regime.ts`), so it is not false, but the
page does not mention the declared override as the path for changing an
action. That line is outside this card's file surface. Noted, not filed.
## Verification
Gates were run on the final head `17e0285f` (`git rev-parse --short
HEAD`), after a whole-tree `turbo run build` (lock `VERDICT command-exit
0`).
- `dispatch-gates --commands`: 70 families derived. All 70 exit 0. `pnpm
lint` exits 0.
- `dispatch-gates --ran`: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN.
- The first sweep, on `ea0dcdad`, was red on
`check:skill-top-level-keys`: one compression had reworded the sentence
that gate quotes ("the top level refuses it and the stack fails to
load"). `17e0285f` restores the sentence and pays the bytes elsewhere.
<details><summary>Gate exit codes on 17e0285</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-skills-token-ratchet.mjs :: exit 0
node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:skill-docs :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:overlay-whitelist-table :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-compatibility :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:skill-top-level-keys :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm lint :: exit 0
```
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4czCxwMepBDxUtozyne)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
docs(skills): the platform skill teaches the declared action override and its access-widening refusal
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
-**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).
11
+
-**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.
Disable it with `defineStack(config, { strict: false })` (objects another plugin provides).
86
81
87
82
### Compile Artifact and Runtime Metadata Boundary
88
83
@@ -116,7 +111,7 @@ manifest: {
116
111
```
117
112
118
113
**`manifest.engines.protocol`:** the metadata-protocol major the app is authored
119
-
against (stamped by `create-objectstack`; the example apps carry it). The runtime
114
+
against (stamped by `create-objectstack`). The runtime
120
115
checks it **before it loads anything** and refuses an out-of-range app at the
121
116
boundary with the exact migration command. Change it when you deliberately move
122
117
to a new protocol major — never to silence a mismatch.
@@ -125,9 +120,9 @@ to a new protocol major — never to silence a mismatch.
125
120
126
121
**`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.
127
122
128
-
**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`.
123
+
**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`.
129
124
130
-
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**).
125
+
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`.
131
126
132
127
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.
133
128
@@ -175,7 +170,7 @@ what the platform owns.
175
170
The five retired remote templates (`todo`, `compliance`, `content`,
176
171
`contracts`, `procurement`) are refused by name.
177
172
178
-
→ Moved verbatim to[references/bootstrap.md](./references/bootstrap.md) § Scaffolding Command.
Copy file name to clipboardExpand all lines: skills/objectstack-platform/evals/config-plugins-ops.json
+19-1Lines changed: 19 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -219,7 +219,7 @@
219
219
{
220
220
"id": 13,
221
221
"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?",
222
-
"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).",
222
+
"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).",
223
223
"files": [],
224
224
"assertions": {
225
225
"must_contain": [
@@ -249,6 +249,24 @@
249
249
"PUT /api/v1/meta/view/crm_account_grid"
250
250
]
251
251
}
252
+
},
253
+
{
254
+
"id": 15,
255
+
"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?",
256
+
"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.",
0 commit comments