Skip to content

Commit 49ebeda

Browse files
committed
merge origin/main (os-regen artifacts taken from main; regeneration follows)
2 parents 2328262 + 034f5a3 commit 49ebeda

52 files changed

Lines changed: 6682 additions & 452 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/formula': minor
3+
---
4+
5+
Add `current_user.can(object, verb)` — the permission predicate — to the CEL engine, together with the data it is answered from.
6+
7+
`Clause-②: yes` — a new callable name widens the authorable surface. Purely additive: nothing is removed, renamed or narrowed, and every expression that evaluated before evaluates the same way.
8+
9+
**What you can write now**
10+
11+
```cel
12+
current_user.can('crm_lead', 'edit')
13+
```
14+
15+
`can` is registered **receiver-only**, so it is called ON the acting subject (`current_user`, or its `user` / `ctx.user` / `os.user` aliases — the same object). A bare `can(object, verb)` is deliberately not registered and keeps faulting: a permission question with no subject has no meaning.
16+
17+
The verb vocabulary is the closed table `OBJECT_PERMISSION_VERBS` in `@objectstack/spec/security` — `read`, `create`, `edit`/`update`/`write`, `delete`/`remove`, `export`, `transfer`, `import`. A verb outside it is refused loudly rather than answered `false`. The answer folds the super-user bits exactly as the enforcement door does, so a predicate and the server's 403 cannot disagree.
18+
19+
**What a call site must pass**
20+
21+
`EvalContext` gains `permissions` — a pure data map, object name → `EffectiveObjectPermission`, which is the `objects` map of the published `/auth/me/permissions` response, unchanged. Build it through the new `toEvalPermissions(response.objects)`, which refuses a payload that is not that shape.
22+
23+
```ts
24+
import { toEvalPermissions } from '@objectstack/formula';
25+
26+
const permissions = toEvalPermissions(mePermissions.objects);
27+
ExpressionEngine.evaluate(predicate, { user, record, permissions });
28+
```
29+
30+
**With no permission data in the context, `can` THROWS** (`ok: false`, `kind: 'runtime'`) and names the missing input. It never answers `true` (which would reveal what the subject may not see) and never answers a silent `false` (which would hide a gated element from everyone, indistinguishable from a real denial). An *empty* map is a real answer and evaluates to `false`, as does an object the map does not mention.
31+
32+
**Also new, all additive**: `EvalPermissions` and `PermissionBinding` types, `registerPermissionPredicate()`, and an optional fourth argument on `registerStdLib()` carrying the binding. Existing three-argument calls are unaffected.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
Publish the object-permission VERB vocabulary and the effective-entry reader from `@objectstack/spec/security`.
6+
7+
`Clause-②: yes` — new exported names on a published surface. Purely additive: no export is removed, renamed or narrowed, and no schema changes shape.
8+
9+
**New exports**
10+
11+
- `OBJECT_PERMISSION_VERBS` — the closed verb → `allow*` bit table. Derived from the bare verbs of the object-permission key aliases (`read`, `create`, `edit`/`update`/`write`, `delete`/`remove`, `export`, `transfer`) plus one row that is not derivable and is recorded as a deliberate choice: `import` → `allowCreate`, because importing rows is creating rows. `restore` / `purge` are absent, as they are on the alias table since their bits were retired.
12+
- `OBJECT_PERMISSION_VERB_NAMES` — the same vocabulary, sorted, for a refusal message to name in full.
13+
- `resolveObjectPermissionVerb(verb)` — the only supported read of the table. Use it rather than indexing the record: a direct index answers `toString` with a function, which a truthiness check reads as a grant.
14+
- `objectPermissionGrants(permission, target)` — whether one `EffectiveObjectPermission` entry grants a bit, folded the way the enforcement path folds it: `viewAllRecords` or `modifyAllRecords` grants read; `modifyAllRecords` grants edit, delete and transfer but never create; `export` is `grant ∧ read`. An absent entry and an all-`false` entry both answer `false`.
15+
- `ObjectPermissionVerbTarget` — the `allow*` bit type a verb can resolve to.
16+
17+
**Why they are published**: `@objectstack/formula`'s new `current_user.can(object, verb)` predicate reads a `/auth/me/permissions` map, and a client rendering the same capability reads the same map. One table and one fold, published once, so the predicate an author writes and the 403 the server returns cannot answer differently.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`AnchorBindingContext`'s boot half names the stack's capability DECLARATIONS, not the `sys_capability` rows the seeder has not written yet
6+
7+
The docblock named two sources for `declaredCapabilities`: at boot 「the
8+
`sys_capability` rows carrying `managed_by: 'package'` provenance」, at authoring
9+
time the stack's own `capabilities` array. The boot half carried an ordering
10+
precondition the sentence never stated, and a caller following it literally
11+
lands on the defect the input exists to remove.
12+
13+
`runBootstrap` (`@objectstack/plugin-security`) awaits `bindBaselineToEveryone`
14+
— the ADR-0090 D5 anchor binding, the boot call site that consults
15+
`describeHighPrivilegeBits` — BEFORE it calls `bootstrapDeclaredCapabilities`,
16+
the seeder that WRITES those `managed_by: 'package'` rows. The order is fixed by
17+
two other constraints stated at that call site: the binding must follow the
18+
seeding of the `everyone` anchor it binds to, and precede the audience-binding
19+
suggestion reconciliation. So on a first boot the table is EMPTY at exactly the
20+
moment the docblock said to read it, and this docblock's own 「omission refuses」
21+
property turns that emptiness into a silent refusal of every declared token —
22+
the app's own `isDefault` set unbindable at the `everyone` anchor, which is the
23+
defect #17811 introduced the input to remove.
24+
25+
The boot half now names the DECLARATIONS, read through the seeder's own two-step
26+
— the ObjectQL registry first, the metadata service as the fallback — which is
27+
what `readDeclaredCapabilityContext` (`@objectstack/plugin-security`, #18535)
28+
already implements, so the contract text and its one runtime consumer now
29+
corroborate each other instead of contradicting. The `sys_capability` rows stay
30+
a valid source, qualified: only once the seeder has written them, which is where
31+
an admin-surface or post-boot caller reads them.
32+
33+
⛔ No behaviour changes. The diff is comment text: `git diff` against the branch
34+
point over `src/security/high-privilege.ts` changes **0** non-comment lines (the
35+
same predicate reads 33 on that file's own #17811 commit, which is the control
36+
proving it fires). No predicate, no type, no export, no accept set moves.
37+
38+
**This is shipped, which is why it carries a changeset rather than
39+
`skip-changeset`.** `src/security/high-privilege.ts` is NOT shipped as source —
40+
`@objectstack/spec`'s published `files[]` takes `src/**/*.zod.ts`, and this file
41+
is not one (`npm pack --dry-run` lists 2021 files and excludes it, with the
42+
sibling `src/security/permission.zod.ts` present as the lit control). Its
43+
published reach is the emitted declarations, and they move: the new clause is
44+
present in `dist/security/index.d.ts` and `dist/security/index.d.mts`, both in
45+
that same shipped list, with the superseded spelling absent from every built
46+
declaration file and the docblock's unchanged neighbouring sentence present in
47+
the same two as the lit control.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/client": patch
3+
---
4+
5+
docs(client): the published README's `packages.install` example is a manifest `ManifestSchema` actually accepts (#18607)
6+
7+
The example shipped in the `@objectstack/client` npm tarball was refused on three
8+
counts when parsed against the contract its own call site declares
9+
(`PackageInstallRequestSchema`, whose `manifest` key is `ManifestSchema`):
10+
`invalid_type` at `[manifest, id]`, `invalid_value` at `[manifest, type]` — both
11+
required and absent — and `unrecognized_keys` at `[manifest]` for a `label` key
12+
that `ManifestSchema`'s `strictObject` close refuses by name.
13+
14+
```diff
15+
await client.packages.install({
16+
- name: 'vendor_plugin',
17+
- label: 'Vendor Plugin',
18+
+ id: 'com.vendor.plugin',
19+
+ type: 'plugin',
20+
+ name: 'Vendor Plugin',
21+
version: '1.0.0',
22+
});
23+
```
24+
25+
`label` is not a root manifest key and never was: the root shape declares `name`
26+
for the human-readable string (measured — `ManifestSchema` declares 25 root keys
27+
and `label` is not among them), so the example's `label` value moves to `name`
28+
and the machine identifier becomes the reverse-domain `id` the key documents.
29+
`type: 'plugin'` is the enum member the example's own subject names — a
30+
general-purpose functionality extension, not the consumer-installable `app`
31+
bundle. Required root keys, read off the schema rather than the prose: `id`,
32+
`name`, `type`, `version`.
33+
34+
Nothing parses that contract at the install door today, so the example "worked"
35+
by being posted unvalidated — which is what made it a timed charge rather than a
36+
live outage: closing the door turns a silently-wrong published example into a
37+
loudly-broken one for every reader who copied it.
38+
39+
Pinned in `packages/client/src/readme-package-install-example.test.ts`, which
40+
parses every `packages.install` manifest literal in this README against that
41+
schema and fails if the corpus is ever empty.
42+
43+
Clause-②: no
44+
45+
No schema, export, type or runtime behaviour changes. It ships because the README
46+
is listed in this package's `files[]` and is the first thing a new integrator
47+
copies.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`src/conversions/registry.ts` — the `connector-rate-limit-config-removed` entry no longer asserts that `retryConfig` and the connector timeouts "are live" (#18614). The assertion was measured false; the ledger seeded by #18582 had already recorded the correction on the other side.
6+
7+
The comment conflated two different statements. That the rate-limit retirement left those keys *in place* is true and is kept — it is what the fixture's single notice demonstrates. That they are *live* was never measured by that entry and is false: the read-probe for `retryConfig`, `connectionTimeoutMs` and `requestTimeoutMs` finds no consumer anywhere outside `packages/spec` (the sibling key `providerConfig`, on the same schema, fires on the identical probe), no retry loop reads a strategy or a backoff, every timeout occurrence outside the spec is a write of the literal `30000` so a def satisfies the post-parse `Connector` type, and `ConnectorProviderContext` carries none of the three — so a provider factory cannot read them either. `liveness/connector.json` classifies all ten rows `dead` and is now cited as the authority.
8+
9+
Nothing is retired here and no schema moved: ADR-0049 owes these keys a decision, which the corrected comment states rather than pre-empts. The text ships — `tsup` preserves comments, so these bytes reach `dist/index.js`, `dist/index.mjs` and the `shared`/`browser` bundles inside the published tarball, which is why this is a `patch` and not `skip-changeset`.
10+
11+
Clause-②: no
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
`os validate` runs the per-package author-time rule pass `os build` already ran — the false-clean residue #17069 left one layer down.
6+
7+
`os build` runs the artifact's authoring rules **twice**: once over the union-folded stack, then a second `runAuthoringRules('build', …)` pass over each `artifactPackages(…)` entry with `packageBodyAsStack(…)` as resolution context, de-duplicated against the union run. `os validate` ran the union pass and stopped — it imported neither seam. By `compile.ts`' own description the survivors of that second pass are "exactly the set the union could not see", so that whole set was findings `os build` reported and `os validate` **structurally could not**. The direction is false-clean, and on the worse door: the fast pre-flight is what an author runs *before* shipping, so its clean bill of health is the strongest false assurance the three commands can give.
8+
9+
Measured on `origin/main` 09e16a574 over `examples/app-multi-package`, both commands exiting 0:
10+
11+
```
12+
os build --json warnings: 4 <- 3 union + 1 per-package survivor
13+
os validate --json warnings: 3 <- the survivor is the defect
14+
```
15+
16+
After: both report 4, the same set, in the same order.
17+
18+
**The loop is now one seam, not two copies.** `runPerPackageAuthoringRules` lives beside `artifactPackages` / `packageBodyAsStack` in `utils/artifact-packages.ts`, whose header already forbids a second copy of that shape by name. What would have drifted between two hand-written loops is not the package reading but the **verdict** — the de-duplication key, the severity split, the `where` prefix. `os build`'s observable output is unchanged (text face byte-identical modulo timings; `--json` payload identical).
19+
20+
**Severity mapping is `os build`'s, unchanged.** A per-package `error` refuses (exit 1); an advisory joins `warnings`. So `os validate` is narrowed only to the bar the command that *ships* already holds: every input it can now refuse is one `os build` already refuses, which means **nothing that builds today stops validating**. No newly-refused input could be exhibited on any fixture — across the repo's own two-package example and three constructed variants the observable change is advisory-only, because `packageBodyAsStack` hands each package the artifact's whole `packages[]` as resolution context and the reference-integrity suite resolves object names through it. Graded `minor` rather than `patch` for the new observable step line, the new advisories and the newly reachable non-zero exit; ⛔ **not** declared breaking, because the narrowing could not be exhibited and is bounded by an existing gate.
21+
22+
Unchanged and out of scope: the ADR-0130 D4 union fold (#17069, fixed — `authoringRuleUnionStack` is in both commands), `--json` rendering (#11727), and disagreements *within* the per-package pass's verdicts (#18204). `os lint` still runs the union pass alone; its `artifactPackages` / `packageBodyAsStack` imports serve its own intra-package duplicate-name advisory, not the shared table.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`ToolExecutionContext.userMessageText` now cites the cloud decision as `cloud ADR-0025`, not as a bare number that resolves to this repo's plugin-packaging ADR
6+
7+
The docblock read `(cloud, post-ADR-0025)`. The parenthetical says the layer is
8+
cloud, but the id was spelled bare — and a bare id resolves against *this*
9+
registry, where `ADR-0025` is
10+
[Plugin Package Distribution](../docs/adr/0025-plugin-package-distribution.md):
11+
a real record about `.osplugin` artifacts, code-plugin trust tiers and
12+
marketplace install. Nothing in it decides who owns the agent route.
13+
14+
That is worse than citing a number nobody has. A dangling id stops a reader; an
15+
id that resolves lets them believe they read the right page and walk away with
16+
the wrong decision. AGENTS.md Prime Directive 13 is explicit — an ADR "lives in
17+
the repository whose code it governs", and a cloud decision is cited as
18+
`cloud ADR-NNNN`, "never as a bare number".
19+
20+
The line now reads `(cloud, post-cloud ADR-0025)`, which is verbatim what the
21+
sibling member `confirmedBlueprintIdentity` two declarations below already says.
22+
The two were deliberately inconsistent while this was open; they are consistent
23+
again.
24+
25+
Docblock prose only — no type, no export and no runtime behaviour changes. The
26+
published `.d.ts` carries the comment, which is why this ships as a patch rather
27+
than silently.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`BatchUpdateRequestSchema`'s cap comment no longer calls the batch-size cap "DEPLOYMENT policy". It is embedder-only, and this correction narrows the claim onto what is actually reachable.
6+
7+
`packages/spec/src/api/batch.zod.ts` ships in this package's tarball (`files[]` carries `src/**/*.zod.ts`), so the sentence a reader finds beside `records` is published text. It told them the cap — `RestServerConfig.batch.maxBatchSize`, 1..1000, default 200 — was deployment policy, i.e. something an operator deploying this platform could move. No shipped boot path makes that true.
8+
9+
**What the comment says now.** The cap keeps its span and its default as schema facts; the reachability sentence says who can write it. A `RestServerConfig` is the ARGUMENT a host passes when it constructs the server, and there is exactly one door: `createRestApiPlugin({ api })`. Neither shipped boot path opens it with a `batch` config — `os serve` forwards exactly two keys out of the stack config's `api:` block (`api.enableProjectScoping`, `api.projectResolution`) and the dev plugin calls `createRestApiPlugin()` with no config at all. A CLI-started deployment therefore always gets the default of 200, and no flag, config file or CLI option moves it; only the embedding host reaches anywhere in the 1..1000 span.
10+
11+
**Nothing executable moves.** No schema key is added, removed or renamed, no accept set widens or narrows, no export changes, and no runtime behaviour is touched. `records` still carries shape only, the cap is still enforced at the route, and `.min(1)` is still absent. The diff is comment text inside one `lazySchema` factory.
12+
13+
**Why this shipped as its own correction.** The same false claim had four other carriers, all already corrected under the same 2026-09-07 ruling: this package's `RestServerConfigSchema` docblocks and WHO CAN WRITE THIS CONFIG header, `enforceBatchSize` in `@objectstack/rest`, and the `data-api` and `http-protocol` reference pages. This was the fifth, and it carried the exact phrase struck from `enforceBatchSize` one package over. The wording is copied from those landings rather than invented, so the five now read the same way — as does the per-key REACHABILITY row in `liveness/batch_endpoints.json`, which also ships here.
14+
15+
Clause-②: no — comment text only. No authorable key moves, no export is added or removed, and no accept set changes in either direction.

‎.changeset/spooky-poems-repeat.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Say it out loud when a `.refine()` never reaches the published JSON Schema.
6+
7+
`z.toJSONSchema()` has no arm for a `custom` check, so every rule written as a
8+
`.refine()` / `.superRefine()` is enforced by the runtime and absent from the
9+
`json-schema/` tree that ships inside this package — a published file that is
10+
WIDER than the Zod type it was generated from, in the direction where an
11+
author's (or an AI's) validator says yes and the platform then says no. Measured
12+
on zod 4.4.3: 688 refinement sites across 240 published schemas, none of which
13+
projected anything.
14+
15+
Nothing about what the schemas accept changes. Each affected file now carries an
16+
`x-dropped-refinements` annotation naming the paths whose rules it does not
17+
state — `x-` keywords are ignored by every validator, so the accepted document
18+
set is byte-for-byte what it was — and the generator reports the population on
19+
every run and refuses to grow it silently
20+
(`packages/spec/dropped-refinements.baseline.json`).
21+
22+
Clause-②: no

0 commit comments

Comments
 (0)