Repository navigation
Commit d4aaeaf
fix(runtime): server-composed clone and re-flag bodies drop a stored protection block before the save door (#23188)
Fixes #23120
Clause-②: no
## What changes
Two runtime doors re-save a body the server composes from a stored row.
Both now drop a `protection` block before the body reaches
`saveMetaItem`. This follows triage's decision on the card (comment
6136379484): one rule for both doors, no new write face, no `source`.
- `packages/runtime/src/flow-clone.ts`: `FLOW_CLONE_DROPPED_KEYS` now
names `'protection'`. It is spelled the way
`PERMISSION_SET_CLONE_DROPPED_KEYS` spells it, because no spec export
names the block on its own. The derived spreads are unchanged.
- `packages/runtime/src/domains/packages.ts`: the publish-drafts
visibility flip builds `{ ...app, _unpublished: false }` as before, then
deletes `protection` from that copy only. The app object the read
returned is not mutated.
Neither door states a `source` or a write face, so every other save-door
gate still judges both bodies. The block binds nothing: only the package
loader translates it into a lock (`applyProtection`). Dropping it
changes no lock and no verdict.
## Measured
**`FLOW_CLONE_DROPPED_KEYS`.** Before, 9 keys: `_diagnostics`, `_draft`
(from `METADATA_READ_DECORATIONS`) and `_lock`, `_lockReason`,
`_lockSource`, `_provenance`, `_packageId`, `_packageVersion`,
`_lockDocsUrl` (from `Object.keys(MetadataProtectionFields)`).
`protection` was in neither source, so the card's premise holds for the
flow door. After: the same 9, plus `protection`.
**The re-flag body.** Before: `{ ...app, _unpublished: false }`, where
`app` is a row from `getMetaItems({ type: 'app', packageId })`. That
read serves a stored row's block as stored (measured below). After: the
same body without `protection`.
**Where the block comes from.** `FlowSchema` declares `protection`, so
`registerFlow`'s `FlowSchema.parse` keeps it on the flow the engine
holds, and `getFlow` returns that object to the clone. A flow the
package loader registered arrives already stripped by `applyProtection`.
A flow bound from a row stored before the save door's refusal does not.
**Reach, at the public doors, on a real showcase boot** (`bootShowcase`
with a database file, through the in-process HTTP app). How a legacy row
was made: the flow and the app were saved through the doors without a
block, and the block was then written into the stored row's `metadata`
JSON with the data engine under the system context. That is the only way
such a row can exist now, because the door refuses the block. A cold
boot on the same database file then bound the rows through the real
path. The script was a one-off under `packages/qa/dogfood/test`, run
twice and deleted. It is not part of this diff.
| Step | Before: runtime dist built from source identical to
`dc93781c19` for both files | After: runtime dist built at `ee0cbaa494`
|
|:--|:--|:--|
| `PUT /meta/flow/NAME` with a block (why no new row can carry one) |
400 `VALIDATION_ERROR` | 400 `VALIDATION_ERROR` |
| Stored legacy flow row and app row carry the block after the cold boot
| yes, yes | yes, yes |
| `GET /automation/NAME` (the engine-held flow) carries the block | yes
| yes |
| `POST /automation/LEGACY/clone` | **400 `VALIDATION_ERROR`**,
`issues[0].path` is `protection`, 0 rows saved, the copy reads 404 |
**200**, 1 row saved, no block, the copy reads 200 |
| Control: `POST /automation/PLAIN/clone` | 200, row saved without a
block | 200, row saved without a block |
| `POST /packages/LEGACY_PKG/publish-drafts` | **200 with the save
door's refusal under `unhideError`**. The row is still `_unpublished:
true` and still carries the block | **200, `unhiddenApps` names the
app**. The row is `_unpublished: false` with no block |
| Control: `POST /packages/PLAIN_PKG/publish-drafts` | 200,
`unhiddenApps` names the app, row `_unpublished: false` | the same |
The clone's refusal message named a block on
`reach23120_legacy_flow_copy`, a flow the admin was creating and had
never sent a block for.
## Tests
- New pins, one per door, each with a no-block control:
-
`packages/runtime/src/domains/automation-tenant-authored-write.test.ts`.
A real protocol over a real `SchemaRegistry`, and the save spied, as the
file's other cases do. The source is `FlowSchema.parse` output carrying
a block, as the engine binds a stored tenant row. Asserted: 200; the
body handed to `saveMetaItem` has no `protection`; nothing else changed;
no `source` was stated. Control: a no-block source saves exactly `{
...definition, name, label, status: 'draft' }`.
-
`packages/runtime/src/domains/packages-unpublished-reflag-protection.test.ts`
(new). The publish-drafts door over a protocol double whose
`getMetaItems` answers the stored rows. Its `objectql` service is a
registry only, so it adds no engine double. Asserted: 200;
`unhiddenApps`; no `unhideError`; the re-saved body equals the stored
row minus `protection`, with `_unpublished: false`; the row the read
returned is unmutated. Control: a no-block app re-saves `{ ...stored,
_unpublished: false }` exactly.
- Both spell `protection` literally rather than reading it from the
constant, so removing either drop reds its pin.
- Before the fix, at `fb9f991a30`/`477e4f530d` with the pins committed:
each block pin red on `expected … to not have property "protection"`,
each control green.
- **Ablation**, from the committed fix (`ee0cbaa494`), inside
`scripts/pm/os-verify-lock.sh`, through `scripts/ablation-replace.mjs`
(WRAP mode) plus an outer `trap` that restores with `git checkout HEAD
--` on the absolute path. The subjects are imported from source by
relative path, so no `dist/` leg applies. Predicted before running: each
leg reds exactly its own door's block pin, 1 failed / 9 passed.
- Leg A, `'protection',` deleted from `FLOW_CLONE_DROPPED_KEYS`: anchor
1 to 0, blob `b2579ea10d` to `c551a850dc`. **1 failed / 9 passed**: the
flow block pin red, both controls and the re-flag pin green. Restored:
blob equals HEAD `b2579ea10d`, `git diff HEAD` empty.
- Leg B, `delete item.protection;` deleted from the flip: anchor 1 to 0,
blob `4c9541ee97` to `660bf46dd2`. **1 failed / 9 passed**: the re-flag
block pin red, both controls and the flow pin green. Restored: blob
equals HEAD `4c9541ee97`, `git diff HEAD` empty.
- At `e3cb0b4deb` (this branch with `origin/main` `e540e7ffb9` merged,
dependency closure rebuilt):
- `pnpm --filter @objectstack/runtime test`: Test Files 364 passed
(364); Tests 5178 passed, 19 skipped (5197). `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/runtime typecheck`: `VERDICT command-exit
0`. The test layer is held at its ledger, 27 files / 190 errors / 68
signatures. `tsc --listFiles -p tsconfig.test.json` puts both test files
in the program (2 of 2, 1414 files), with 0 of the 190 errors in any
file this PR touches.
## Gates
At `e3cb0b4deb`, `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 64 commands with no paths (5 paths
vs merge base `e540e7ffb`). That is identical to the derivation over the
claim's paths. Each command ran with its exit code captured before any
pipe.
- 63 exited 0 on the first pass.
- `pnpm check:dual-build-cjs-loads` first answered `PREREQUISITE NOT
MET` (exit 3) for 8 packages with no `dist/`. Those were built (turbo,
all 58 tasks from cache), and the rerun exited 0: "108 published require
entry point(s) across 67 package(s) load".
- `dispatch-gates --ran`: "64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN".
**Lint, narrowed.** `eslint --no-inline-config --format json` over the 4
changed `.ts` files: 4 linted, 0 errors, 0 warnings, none ignored
(`isPathIgnored` false). The changeset `.md` is outside eslint's
population (`isPathIgnored` true). `eslint.config.mjs` never enables
type-aware linting (no `parserOptions.project`, as stated in the config
itself), so this diff cannot move any untouched file's verdict. The full
`pnpm lint` belongs to CI.
## Acceptance notes
- Read, not measured: the other runtime `saveMetaItem` sites (`POST
/automation`, `PUT /automation/:name`, the `/meta` dispatcher save) save
the caller's own body. A refusal there names a block the caller sent,
which is the remedy #23096's changeset documents. In
`metadata-protocol`, the server's own re-saves either state a `source`
or face (stored migration, duplication, unmanaged import) or, for commit
revert/rollback and draft promotion, write rows without going through
`saveMetaItem`. So no third server-composed door meets this refusal.
Nothing filed.
- A legacy row keeps its bytes until something rewrites it. `GET
/meta/flow/NAME` and `GET /automation/NAME` still serve its block
(measured above), and a Studio round trip is still refused with the
documented remedy. A flip now rewrites an app's row without the block. A
clone writes a new row without it and leaves its source row alone.
- Release order: `.changeset/23096-save-door-protection.md` is in
`main`'s unreleased stock, and this PR adds
`.changeset/23120-server-composed-protection-drop.md`
(`@objectstack/runtime` patch). If this lands before the next release is
cut, both ship in one release, as triage requires.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8814211 commit d4aaeaf
5 files changed
Lines changed: 224 additions & 10 deletions
File tree
- .changeset
- packages/runtime/src
- domains
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
21 | 24 | | |
22 | 25 | | |
23 | 26 | | |
| |||
32 | 35 | | |
33 | 36 | | |
34 | 37 | | |
| 38 | + | |
35 | 39 | | |
36 | 40 | | |
37 | 41 | | |
38 | 42 | | |
39 | 43 | | |
40 | 44 | | |
41 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
42 | 50 | | |
43 | 51 | | |
44 | 52 | | |
| |||
60 | 68 | | |
61 | 69 | | |
62 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
63 | 74 | | |
64 | 75 | | |
65 | 76 | | |
| |||
178 | 189 | | |
179 | 190 | | |
180 | 191 | | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 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 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1729 | 1729 | | |
1730 | 1730 | | |
1731 | 1731 | | |
| 1732 | + | |
| 1733 | + | |
| 1734 | + | |
| 1735 | + | |
| 1736 | + | |
| 1737 | + | |
| 1738 | + | |
| 1739 | + | |
| 1740 | + | |
| 1741 | + | |
| 1742 | + | |
| 1743 | + | |
| 1744 | + | |
| 1745 | + | |
| 1746 | + | |
| 1747 | + | |
| 1748 | + | |
| 1749 | + | |
| 1750 | + | |
| 1751 | + | |
1732 | 1752 | | |
1733 | 1753 | | |
1734 | 1754 | | |
1735 | | - | |
1736 | | - | |
1737 | | - | |
1738 | | - | |
1739 | | - | |
1740 | | - | |
1741 | | - | |
| 1755 | + | |
1742 | 1756 | | |
1743 | 1757 | | |
1744 | 1758 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
125 | | - | |
126 | | - | |
| 125 | + | |
| 126 | + | |
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
138 | 148 | | |
139 | 149 | | |
140 | 150 | | |
141 | 151 | | |
| 152 | + | |
142 | 153 | | |
143 | 154 | | |
144 | 155 | | |
| |||
0 commit comments