Repository navigation
Commit abe8f28
Part of #21454
Clause-②: yes
The stored-metadata-body family serves a stored metadata body only as
its type's read projection, and the stored content hash over it only in
keyed form, at every door. Step 1 of this card measured three in-process
reader contexts serving the two stored-metadata tables as stored, body
and hash alike. The action contexts run elevated, so a member who
invokes the action receives the same answer as an administrator. Triage
ruled disposition A: apply the family's projection and keyed serve at
the reader-context seams, consume the shared functions, and leave the
engine alone. This PR does that for every context the measurement found.
It is `Part of`, not `Fixes`: the flow reader that triage folded into
this card was measured and is reached, but its fix lies outside this
claim's surface (see "Not closed by this PR").
Disclosure discipline: classes, doors, roles, codes and forms only.
## How
- **`packages/runtime/src/stored-metadata-reader-seam.ts` (new).** One
serve, built only from the generic data door's own functions:
- the type companion for a projection that names the body without the
type;
- the body projection, which consumes the family's one redactor in
`@objectstack/spec/kernel`;
- the keyed serve, under the crypto provider's digest or, when none is
registered, the data door's own process-scoped ephemeral key.
`serveStoredMetadataRead` serves one read.
`serveStoredMetadataReadsThrough` serves a scoped API: `find`, `findOne`
and `aggregate` on a family object, through every context the API can
derive (`sudo()`, `withRunAs(...)`, a `transaction(...)` callback's
context and the context `beginTransaction()` returns). It is idempotent,
so a hash is never keyed twice. Writes, `count` and every other object
pass through unchanged.
- **`sandbox/body-runner.ts`, `buildSandboxApi`.** Both body faces (hook
and action) get their `ctx.api` here, every fallback included. That API
is now served through the seam, so a hook body that copies what it read
can only copy the projected form.
- **`action-execution.ts`.** Two call sites are served through the seam:
`buildActionApi`, the `ctx.api` an action body and a host code handler
both receive, and `buildActionEngineFacade`'s `find`. Both action doors
(REST `/actions` and MCP `run_action`) build these two, so both doors
are covered.
- **`packages/metadata-protocol/src/index.ts`.** Exports the data door's
four stored-row serve functions and the digest type, so the runtime
consumes them rather than copying them. The exports are additive and
change no behaviour.
- **`scripts/engine-double-contract.pinned.json`.** One new pinned row:
the seam unit test's scoped-API double routes `findOne` through the
engine's predicate. Written by the gate's own `--write`.
**A declared in-place widening.** The handler's `ctx.api` (③ in the
table below) was not one of the two contexts the ruling named. It is the
same scoped context object an action body receives (`buildActionApi`),
and it was measured reaching (administrator and member: stored body,
stored hash). The fix is the same call, in a file this claim already
holds. All four in-place conditions hold, so it lands here and the pins
cover it.
## Readings: before and after
| context | door | role | before | after |
|:--|:--|:--|:--|:--|
| ① action body, `ctx.api.object(...)` | REST actions | administrator,
member | stored body, stored hash (both tables) | projected body, keyed
hash |
| ① action body, inside `ctx.api.transaction(...)` | REST actions |
administrator, member | stored (administrator, measured this run) |
projected, keyed |
| ① hook body that copies what it read into an ordinary record |
data-door insert | administrator | stored content landed in the record |
only projected content lands |
| ① hook body | data-door insert | member | refused, 403
`PERMISSION_DENIED` | unchanged |
| ① action body authored at runtime through `/meta` | metadata, then
actions | administrator authors; administrator and member invoke |
stored | projected, keyed |
| ② handler `ctx.engine.find` | REST actions | administrator, member |
stored (both tables) | projected, keyed |
| ③ handler `ctx.api.object(...)` | REST actions | administrator, member
| stored (measured this run) | projected, keyed |
| control: generic data door | data | administrator | projected, keyed |
unchanged |
| control: generic data door | data | member | refused, 403
`PERMISSION_DENIED` | unchanged |
For every row marked "after", the pins assert four things:
- no stored credential and no stored hash anywhere in the answer;
- the row's non-credential configuration is present, so the body is the
projection of this row, not one that was withheld;
- the served hash equals the keyed value the data door serves for the
same row, which shows one serve and one key.
## Tests
- **Pins.**
`packages/runtime/src/stored-metadata-reader-contexts.pin.test.ts` is
step 1's probe with its context cases flipped from RECORD to ASSERT. It
boots the `bootStack` plugin set, in order, once in `beforeAll` (never
inside a case) and runs 15 cases.
`packages/runtime/src/stored-metadata-reader-seam.test.ts` holds 8 unit
cases for the routes around the seam that the composition does not
exercise one by one. Both files are green at `532c96d70f` (15/15, 8/8).
- **Runtime suite.** At `532c96d70f`, `pnpm --filter
@objectstack/runtime exec vitest run --project local --maxWorkers=2`
answered: Test Files 311 passed (311), Tests 4393 passed, 19 skipped.
`--project repo` at `265f1c1f32`: Test Files 3 passed, Tests 751 passed.
- **metadata-protocol suite.** At `265f1c1f32`, `pnpm --filter
@objectstack/metadata-protocol exec vitest run --maxWorkers=2` answered:
Test Files 205 passed, 3 skipped; Tests 3092 passed, 19 skipped. After
that sha the package's only change came in through the main merge, and
it did not touch this package.
- **Typecheck.** At `532c96d70f`, `pnpm --filter @objectstack/runtime
typecheck` (`tsc --noEmit` plus `check:test-typecheck`, OK, ledger
unchanged) and `pnpm --filter @objectstack/metadata-protocol typecheck`
were both green.
- **Built artifacts.** Both the CJS and ESM builds of
`@objectstack/runtime` load, and both builds of
`@objectstack/metadata-protocol` export the four functions.
## Reverse verification (forms only)
The fix was committed first, at `265f1c1f32`. Then:
- **Mutation leg.** The three exercised seam calls (the sandbox API, the
action `ctx.api`, the engine facade's `find`) were replaced through
`scripts/ablation-replace.mjs`. Each anchor went from 1 to 0 and each
blob changed. The runtime was rebuilt, and
`scripts/ablation-dist-preflight.mjs` found the mutation marker in both
dist bundles. The pins then answered 10 failed, 13 passed. Every context
case failed with "the stored credential reached the answer". The
precondition, the three controls, the member hook refusal and the 8 seam
unit cases stayed green.
- **Restore leg.** A trap ran `git checkout HEAD --` on the two paths.
Both blobs equal HEAD, `git diff HEAD` is empty and the porcelain is
clean. After a rebuild, the preflight with `--absent` was clean and the
pins answered 23/23 green.
- **Direction.** The expected direction, red. `buildActionApi`'s
malformed-envelope fallback is the one seam call no case exercises, so
it was not mutated.
## Gates
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` was derived with no paths at
`532c96d70f` and gave 72 lines. All 72 were run at that head, each
exited 0, and `--ran` reconciled them: 72 derived, 72 run, 0
NOT-MEASURED, 0 UNRUN.
- **CJS load gate.** `check:dual-build-cjs-loads` answered PREREQUISITE
NOT MET on an earlier head. It was re-run once the tree's packages were
built, and it measured 106 entry points across 66 packages.
- **`check:engine-double-contract`** asked for the new pinned row above.
- **`check:slot-lookup`** asked for the pins' handler plugin to read the
engine slot through its contract type. Both are fixed in this PR.
- **Lint, a proven narrowing.** `pnpm lint` (the repo-wide `eslint .`)
was not run locally; CI runs it.
- Population, read from eslint's own config: 6 of the 8 changed paths
are linted. The changeset and the JSON ledger answer "no matching
configuration".
- Count, read from `--format json` at `532c96d70f`: 6 files, 0 errors, 0
warnings.
- Invariance: `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, no typed rules) and carries no cross-file
import rules. Its custom plugins and baselines are per file, so this
diff cannot move the verdict on any untouched file.
## Not closed by this PR (measured; reported on the card for routing)
1. **The flow record-read node.** Triage folded it into this card, to be
measured first. It is reached. Under both run identities, a flow's
record-read node answers the stored body and the stored hash. That
answer goes into the run's declared output, which the flow-action door
returns to its caller, and into any record the flow writes.
- How it was measured: in the `service-automation` tier, on an
in-process engine with the automation service, with the row seeded in
its stored form. No security plugin was loaded, so the run identities
are not role-gated there.
- Why it is not edited here: the keyed serve cannot be reached from
`@objectstack/service-automation`, which does not depend on
`@objectstack/metadata-protocol`. Consuming it there needs either that
dependency edge or the keyed serve moved beside the body primitives in
`@objectstack/spec/kernel`. Both are outside this claim's surface, and
the second is `packages/spec`.
2. **Evaluate shapes through these contexts.** A predicate over the body
column, run through a body's `count`, answers whether a guess matches
(measured, member invoking). The data door refuses these shapes. The
ruled disposition changes only what a read serves, and so does this PR.
3. **An object name the engine's registry does not resolve.** The
engine's in-process verbs hand such a name to the driver, and the driver
can resolve it to a family table.
- The seam judges the object by the family's own predicate on the name,
exactly as the data door does. The data door, however, also refuses an
unregistered name (404 `OBJECT_NOT_FOUND`), and the in-process verbs do
not.
- Measured after this fix: an action body, invoked by an administrator
or a member, still receives the stored body and hash this way.
- This bypasses every name-keyed in-process guard, not only this one, so
the fix belongs in the engine.
4. **A hook bound to a family object.** It receives the stored row in
its own hook context and can copy it (measured: fired by the metadata
door's own save). That row does not come through `ctx.api`, and this PR
does not project it, because a hook's input is also its write-back
channel.
5. **A write through these contexts.** It returns the written row in its
stored form (measured for an elevated body's update). Under dispatch
assumption A3 this is reported, not widened into this PR. The same
measurement shows that an elevated body can write the family tables
directly.
## Acceptance notes
- The engine's `ScopedRepo.execute()` hands a code handler a raw scoped
context that no seam wraps. Its own census found zero callers. That is
engine lane: noted, not filed.
- Changeset: `.changeset/21454-reader-context-family-serve.md`, patch
for `@objectstack/runtime` and `@objectstack/metadata-protocol`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 88fb5e8 commit abe8f28
8 files changed
Lines changed: 872 additions & 4 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- runtime/src
- sandbox
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
193 | 210 | | |
194 | 211 | | |
195 | 212 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
52 | 53 | | |
53 | 54 | | |
54 | 55 | | |
| |||
1496 | 1497 | | |
1497 | 1498 | | |
1498 | 1499 | | |
| 1500 | + | |
| 1501 | + | |
| 1502 | + | |
| 1503 | + | |
| 1504 | + | |
| 1505 | + | |
1499 | 1506 | | |
1500 | 1507 | | |
1501 | 1508 | | |
1502 | 1509 | | |
1503 | | - | |
| 1510 | + | |
1504 | 1511 | | |
1505 | 1512 | | |
1506 | 1513 | | |
1507 | 1514 | | |
1508 | | - | |
| 1515 | + | |
1509 | 1516 | | |
1510 | 1517 | | |
1511 | 1518 | | |
| |||
1677 | 1684 | | |
1678 | 1685 | | |
1679 | 1686 | | |
1680 | | - | |
1681 | | - | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
1682 | 1695 | | |
1683 | 1696 | | |
1684 | 1697 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
60 | 61 | | |
61 | 62 | | |
62 | 63 | | |
| |||
785 | 786 | | |
786 | 787 | | |
787 | 788 | | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
788 | 799 | | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
789 | 804 | | |
790 | 805 | | |
791 | 806 | | |
| |||
0 commit comments