Skip to content

Commit af10b60

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23269-v18-stored-row-fates
2 parents f67be95 + 9c7cfbf commit af10b60

51 files changed

Lines changed: 2933 additions & 214 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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/spec': minor
4+
'@objectstack/rest': minor
5+
---
6+
7+
fix(metadata-protocol)!: a metadata save or delete that names no package addresses the row of the one package the item belongs to, and a name two or more packages hold is refused `409 RESOURCE_CONFLICT`; the delete door can name its package (ADR-0048, ADR-0070)
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no authorable spelling, export or stored shape is removed, renamed or re-shaped, and no stored row is rewritten, converted or deleted, so there is nothing for `objectstack migrate meta` to rewrite. One optional request key is ADDED (`DeleteMetaItemRequestSchema.packageId`, with `?package=` on the REST reset door), a widening that asks nothing of an existing caller. What narrows is two runtime write doors: a save or a delete through the metadata API that names no package, of an item two or more packages hold, is refused, as is a package-less save of an item a read-only package holds; the remedy is in the request a caller sends (name the package), never a rewrite of a package source or a stored row. The other categories are closed on facts: the packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no exported declaration is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12+
13+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
A `sys_metadata` row is keyed by its package (ADR-0048), so one item can hold one row per package. A save that names a package (`?package=` on the REST door, `packageId` in process) writes that package's row. A save that named none always wrote the package-unbound row, even for an item a package owns, and a delete named no package at all: it removed whichever row of the item the store returned first. The draft save already resolved this: a draft that names no package is stored in the package of the item's active row.
16+
17+
**FROM.** Save an item into a package (`PUT /api/v1/meta/app/my_app?package=com.acme.app`), then save it again without `?package=`: the second save answered `200` "Saved app 'my_app' (env-wide, state=active)" and wrote a second, package-less row beside the package's row. The item read kept serving the package's row, so nothing the save carried took effect, and a console "Disable" reported success while the app stayed enabled. The same held for the API, the CLI and agents, and for an overlay row bound to a code package. A `DELETE /api/v1/meta/app/my_app` of an item two packages hold removed one of the two rows, the other was then served, and the receipt said "it no longer exists". Uninstalling a package, or discarding its drafts, deleted each of its items by name the same way, so it could remove another package's row of the same name and leave its own.
18+
19+
**TO.** A direct save, or a delete, that names no package resolves its address from the item's rows (the active rows for a save and a delete, the draft rows for a draft discard):
20+
21+
- **Bound to one package.** The save writes that package's row, exactly as a save naming the package does, and the item read serves it next; the delete removes that row. The receipts name the package: "Saved app 'my_app' (env-wide, package=com.acme.app, state=active)" and "Deleted app 'my_app' (package=com.acme.app) — it no longer exists." Every check on the row the save writes reads that package: its stored lock, a stored credential carried forward, the optimistic lock, and the writable-base rule. The item read that names no package serves that row's `version`, so a read followed by a save or a delete with the served `If-Match` is accepted.
22+
- **Bound to two or more packages.** The save or delete is refused `409 RESOURCE_CONFLICT` and changes nothing. The message names the packages and the remedy: name one with `?package=`.
23+
- **Bound to none.** The save writes, and the delete removes, the package-unbound row, as before, and the receipts read as before.
24+
25+
The delete door gains the address a save has: `DELETE /api/v1/meta/:type/:name?package=<id>` (`packageId` in process, `DeleteMetaItemRequestSchema.packageId`) removes that package's row and no other; `all` and an empty value name no package, as on the save door. A repeated `?package=` is refused `400`. A delete receipt now says when another row of the item is still stored, naming it, instead of claiming the item is gone or back at its artifact default. Uninstalling a package and discarding its drafts name the package, so each removes only its own rows.
26+
27+
**Also refused now**, each as the same save naming the package is refused: a package-less save of an item held by a read-only package (a code package's runtime-only item, or an installed `system`/`cloud` package's) answers `422 WRITABLE_PACKAGE_REQUIRED`; and a package-less save asserting a create (`If-None-Match: *`) of an item a package holds answers `409 METADATA_CONFLICT`.
28+
29+
`packageId: null` in process names no package too, as the request contracts state. The server-side rewrites keep the row they name: `os migrate meta --stored --apply` and the stored flow credential move pass `null` for a package-unbound row they rewrite, and that row is still the one written; package duplication names its target package.
30+
31+
**Unchanged.** A save or a delete that names a package exactly. A draft save, which keeps its own inheritance. An item a code package ships with no stored row: the package-less save still writes the package-unbound overlay, and the read serves it. Package-less rows already stored beside a package's row stay as they are; none is converted. A package-less delete of such an item removes the package's row and its receipt says the package-less row remains.
32+
33+
**Remedy.** For an item two packages hold, send the save or the delete with `?package=` naming the package whose row it edits or removes. A receipt now names the package when the write addressed a package's row; a client that compares receipt text should expect `, package=<id>` after `env-wide` on a save and ` (package=<id>)` after the item on a delete.
34+
35+
`@objectstack/spec`: the `SaveMetaItemRequestSchema.packageId` description now states what an `active` save that names no package writes, where it said the env-local overlay; `DeleteMetaItemRequestSchema` declares the optional `packageId`. `@objectstack/rest`: `package` joins the reset door's closed query set (`META_ITEM_RESET_PARAMS`).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: a page action row's url `target` is judged on the executor the console's action runner actually runs, so a row with no executor key whose `name` is `url` or `navigation` has its `target` judged by the url scheme set
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered ui-page-action-row-name-leg-url-target-judged -->
10+
11+
**BREAKING**: an accept-set narrowing on one key of four published page component rows, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
The page action rows are `action:button` and `action:icon` (`properties`), and each member of an `action:group` or `action:menu` (`properties.actions[]`). The console's action runner dispatches a row on `type || actionType || name`, and every row forwards its executor key (`actionType` on a node, `type` on a member) as `type` beside its `name`. So a row with no executor key runs the handler its `name` names, and `name: 'url'` or `name: 'navigation'` runs the url navigator. The rows judged `target` by the url scheme set only when the executor KEY read `'url'`, so such a row parsed with a target the click refuses.
14+
15+
The rows now judge `target` against the row's effective executor: the executor key, or, when that key is omitted or empty, the `name`, exactly as the runner resolves it. When it resolves to the url navigator, `target` is judged by the same url scheme set as an `actionType: 'url'` row, with no second grammar. A declared executor key still outranks the `name`. `name` is not refused as an executor spelling, and a row whose `name` names any other handler (`navigate_edit`, `convert_lead`, …) is untouched. An `actionType: 'url'` row's refusal is unchanged.
16+
17+
### FROM → TO
18+
19+
| before | what to write instead |
20+
| --- | --- |
21+
| `{ name: 'url', target: 'javascript:…' }` with no `actionType` (or no `type` on a member), or `data:…`, `tel:…` or any other scheme | an `https://` page, a `mailto:` address, or an in-app path that starts with `/` or `./`; or, for a button meant to run something, the executor that does the work (`actionType: 'script'`, `'flow'`, `'api'`) |
22+
| the same with `name: 'navigation'` | the same; the `name` is the runner's url alias, so the target is judged as a url |
23+
| `{ name: 'url', target: 'forecast' }`, `'../forecast'`, `'?tab=2'` | the rooted in-app path, `'/forecast'`, or a `./` path; or declare the executor the row was meant to run |
24+
| `{ name: 'url', target: '//cdn.example.com/x' }` | `'https://cdn.example.com/x'`, or `'/x'` for an in-app path |
25+
| `{ name: 'url', target: '' }` | the address the button was meant to open |
26+
27+
**The one-line fix: give every page action row that has no executor key and is named `url` or `navigation` an `http(s)://` URL, a `mailto:` address, an in-app path that starts with `/` or `./`, or a template that opens with `${` as its `target`, or declare the executor key it was meant to run.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-name-leg-url-target-judged`; the rewrite is by hand, because which address a refused target meant is the author's call.
28+
29+
**What an author now sees.** `os validate`'s props gate reports the refusal at the row's `target` (`properties.target`, or `properties.actions.N.target` for a member). For example, `{ name: 'url', target: 'javascript:alert(1)' }` on an `action:button` gets: ``An `action:button`'s `target` (`name: 'url'` and no `actionType`, so the runner runs the url navigator) opens with `javascript:`, a scheme a url target never opens.``, followed by the reason and the admitted set. Every admitted target parses unchanged.
30+
31+
**Measured producers: none.** No page action row in this repository, in objectui (at its pin `1fb1ced36243` and its main `866763bd`) or in hotcrm (at `0f84c9c`) is named `url` or `navigation`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
fix(runtime)!: the notification inbox, API-key mint and share-link management dispatcher domains decide an anonymous request once, as each domain's first statement, so an anonymous caller is answered 401 whatever the deployment carries
6+
7+
ADR-0138 D2 decides that everything off its closed list of six door classes answers 401, and that "the decision is taken once per domain, as the domain's first statement, never surface by surface". Three dispatcher domains decided it per face, below another answer: `/notifications` below its inbox capability probe, `/keys` below its method check, and `/share-links` inside its management faces, below the share-link service probe. Each now opens with the shared `shouldDenyAnonymous` decision, before any service is resolved and before any face is chosen, so a face added to one of them later is refused to an anonymous caller from the day it lands.
8+
9+
Clause-②: no (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is the status three runtime dispatcher domains answer an unauthenticated caller with, on compositions where another answer used to come first; that caller was refused before and is refused now, and the remedy is to sign in, never a rewrite of a package source or a stored row. 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 nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12+
13+
**BREAKING** (a refusal moved ahead of the answers it used to follow), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
What an unauthenticated caller is answered now, on each domain:
16+
17+
- `/notifications`: `401 UNAUTHENTICATED` on every face. On a deployment with no inbox (a send-only notification provider, or none) the anonymous answer was the `501` capability answer; it is now the 401, and a signed-in caller there still gets the 501.
18+
- `/keys`: `401 UNAUTHENTICATED` for every verb. A verb other than `POST` used to be answered `405` to an anonymous caller; a signed-in caller still gets the 405.
19+
- `/share-links`: `401 UNAUTHENTICATED` on the create, list and revoke faces and on every path the domain does not serve. On a deployment without the share-link service the anonymous answer was `501`; a signed-in caller there still gets the 501. The two token faces, `GET /share-links/:token/resolve` and `GET /share-links/:token/messages`, are unchanged: the capability token admits them (ADR-0138 D2 class 2).
20+
21+
The refusal is the dispatcher's standard envelope, `{ success: false, error: { code: 'UNAUTHENTICATED', message, httpStatus: 401 } }`, with the shared message every other dispatcher domain answers with, so a client reads one anonymous refusal on all of them. Nothing a signed-in caller is served changes, and on the standalone server the booted showcase already answered 401 on the faces it mounts.

‎content/docs/ai/skills-reference.mdx‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,28 @@ ObjectStack ships a set of **domain-specific skills** that teach AI assistants (
1414

1515
## Installing skills
1616

17-
The catalog ships as the npm package [`@objectstack/skills`](https://www.npmjs.com/package/@objectstack/skills), released together with every other `@objectstack/*` package at one shared version. Install it at the version of the `@objectstack/spec` your project has installed — the catalog's version is then your spec's, so the skills teach what that spec accepts — and sync it from `node_modules` into your coding agent's skills directory:
17+
The catalog ships as the npm package [`@objectstack/skills`](https://www.npmjs.com/package/@objectstack/skills), released together with every other `@objectstack/*` package at one shared version. Install it at the version of the `@objectstack/spec` your project has installed — the catalog's version is then your spec's, so the skills teach what that spec accepts — and copy it from `node_modules` into your coding agent's skills directory:
1818

1919
```bash
2020
# 1 · the catalog, at the version of your installed @objectstack/spec
2121
npm install --save-dev "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")"
2222

23-
# 2 · link it from node_modules into your agent's skills directory
24-
npx skills experimental_sync --agent claude-code -y
23+
# 2 · copy it from node_modules into your agent's skills directory
24+
npx skills@1.7.2 experimental_sync --agent claude-code --copy -y
2525
```
2626

27-
Step 2 is the [skills CLI](https://www.npmjs.com/package/skills)'s sync from `node_modules`: it finds the skills under `node_modules/@objectstack/skills/dist/skills/`, links each one into `.agents/skills/<skill>` (and, for `claude-code`, `.claude/skills/<skill>` to that), and records the source and version in `skills-lock.json`. Name your own runtime after `--agent` (`codex`, `cursor`, …). Add `--copy` to write real files instead of links — for `claude-code` the catalog then lands once, in `.claude/skills/`, and nowhere else. A bare `npm install --save-dev @objectstack/skills` installs whichever version the registry tags `latest`, not your spec's — keep the version in step 1.
27+
Step 2 is the [skills CLI](https://www.npmjs.com/package/skills)'s sync from `node_modules`: it finds the skills under `node_modules/@objectstack/skills/dist/skills/`, copies each one into your agent's directory — for `claude-code`, real files in `.claude/skills/<skill>` and nowhere else — and records the source and version in `skills-lock.json`. Commit both with the project. Keep `--copy`: without it the sync writes links into `node_modules`, which is not committed, so a clone of the project gets dangling links and no skills. The CLI is pinned because `experimental_sync` is that tool's own unstable interface; `create-objectstack` runs the same version. Name your own runtime after `--agent` (`codex`, `cursor`, …), one runtime per run — never `--agent '*'`, which copies the catalog into dozens of agent directories at once. A bare `npm install --save-dev @objectstack/skills` installs whichever version the registry tags `latest`, not your spec's — keep the version in step 1.
2828

29-
**Updating.** The catalog is versioned as **one bundle**, and it moves only with your ObjectStack version — you never track or update a skill on its own: bump `@objectstack/skills` with the other `@objectstack/*` packages, then re-run the sync (step 2, same flags).
29+
**Updating.** The catalog is versioned as **one bundle**, and it moves only with your ObjectStack version — you never track or update a skill on its own: bump `@objectstack/skills` with the other `@objectstack/*` packages, then re-run the sync (step 2, same flags) and commit what it changed.
3030

31-
**Which versions exist.** `@objectstack/skills` is published from `18.0.0-next.0` on; there is no 17.x release of it. A project on `@objectstack/spec` 17.x therefore has no catalog bound to its version: step 1 refuses with `ETARGET` (`No matching version found`).
31+
**Which versions exist.** `@objectstack/skills` is published from `18.0.0-next.0` on; there is no 17.x release of it. A project on `@objectstack/spec` 17.x therefore has no catalog bound to its version: step 1 refuses with `ETARGET` (`No matching version found`), and moving it to 18 includes [a skills step](/docs/upgrading#moving-the-dependency-pins). One scaffolder is the exception to the next paragraph: `create-objectstack@18.0.0-next.0` still installed the skills from the repository's `main` with `skills add`, and step 2 skips every skill a `skills add` installed (it prints `Skipped … installed with skills add` and exits 0), so in a project it scaffolded run `npx skills remove --all` — or remove the `objectstack-*` skills by name, as the [upgrade step](/docs/upgrading#moving-the-dependency-pins) does, if you added others — and then steps 1 and 2.
3232

33-
**New projects.** `create-objectstack` installs the skills for one agent runtime while it scaffolds — once, into `.claude/skills/` — and its closing summary prints the command for any other runtime you use.
33+
**New projects.** `create-objectstack` runs both steps while it scaffolds: the `package.json` it writes depends on `@objectstack/skills` at the same version as `@objectstack/spec`, its dependency install puts the catalog in `node_modules`, and step 2 copies it for one agent runtime, into `.claude/skills/`. Its closing summary prints step 2 again — to re-run after an upgrade, and with another `--agent` for any other runtime you use.
3434

3535
```bash
3636
npm create objectstack@latest my-app
3737
```
3838

39-
Through `18.0.0-next.0` the scaffolder takes those skills from the `next` channel below with `skills add`, not from `@objectstack/skills`, and step 2 skips every skill a `skills add` installed (it prints `Skipped … installed with skills add`). To bind a scaffolded project's skills to a version the package is published at, remove them first — `npx skills remove --all` removes every installed skill; name the `objectstack-*` ones instead (`npx skills remove objectstack-data objectstack-ui …`) if you added others — then run steps 1 and 2.
40-
4139
**The `next` channel.** `npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -y` reads the catalog straight from the repository's unreleased `main` — for trying the next version's skills, not for a released project.
4240

4341
---

0 commit comments

Comments
 (0)