Skip to content

Commit 288e5a4

Browse files
os-zhuangclaude
andauthored
fix(metadata-protocol): the ADR-0010 lock gate must not fail open (#5706) (#5736)
`getEffectiveLock` is the only source of truth for the ADR-0010 §3.3 lock gate, and both callers are write-path admission — `assertLockAllowsWrite` (save/publish/rollback) and `assertLockAllowsDelete`. Its overlay read was wrapped in a bare `catch` that fell through to `lock: 'none'`. `'none'` is not a neutral placeholder there: it is the verdict "the author declared no protection", which `evaluateLockForWrite` / `evaluateLockForDelete` turn straight into "allow". A `sys_metadata` read that FAILED therefore became a write that was PERFORMED on an item whose overlay row declared it protected. Measured on origin/main, with the row carrying `_lock` and only the gate's own read rejecting: `saveMetaItem` resolved `success: true` after `update:sys_metadata` on a `no-overlay` item, and `deleteMetaItem` the same on a `no-delete` one — while the same rows read successfully produce 403 ITEM_LOCKED. The audit trail did not compensate: the allowed path writes its ordinary `outcome: 'allowed'` row. Reuses `rethrowUnlessMetadataStoreUnprovisioned` (#5705) rather than inventing a second predicate — an unprovisioned `sys_metadata` genuinely has no overlay row, so `'none'` is the truth and first boot still saves; every other error becomes 503/SERVICE_UNAVAILABLE with the driver error as `cause`. Wire-visible, and deliberate: refusing one uncertain write beats performing one that had to be refused. Unaffected and pinned by regression tests: artifact-level locks (answered from the in-memory registry before the overlay read), a genuine miss on a healthy store, and control-plane kernels. Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5e3c83b commit 288e5a4

3 files changed

Lines changed: 495 additions & 2 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): the ADR-0010 lock gate refuses an uncertain write instead of allowing it (#5706)
6+
7+
`getEffectiveLock` is the single source of truth for the ADR-0010 §3.3 lock
8+
gate, and both of its callers are write-path admission — `assertLockAllowsWrite`
9+
(save / publish / rollback) and `assertLockAllowsDelete`. Its overlay read was
10+
wrapped in a bare `catch` that fell through to `lock: 'none'`.
11+
12+
`'none'` is not a neutral placeholder there. It is the verdict "the author
13+
declared no protection", and `evaluateLockForWrite` / `evaluateLockForDelete`
14+
turn it straight into "allow". So a `sys_metadata` read that **failed** became a
15+
write that was **performed**, on an item whose overlay row declared it
16+
protected. Measured before the fix, with the overlay row carrying `_lock` and
17+
only the gate's own read rejecting: `saveMetaItem` returned `success: true`
18+
after updating a `_lock: 'no-overlay'` item, and `deleteMetaItem` returned
19+
`success: true` after deleting a `_lock: 'no-delete'` one — while the very same
20+
rows, read successfully, produce `403 ITEM_LOCKED`. The audit trail did not
21+
record the miscarriage either: the allowed path writes its ordinary
22+
`outcome: 'allowed'` row, so nothing afterwards showed the write should have
23+
been denied.
24+
25+
**Wire-visible change.** When the lock state cannot be read, `save`, `publish`,
26+
`rollback` and `delete` now fail with `503` / `SERVICE_UNAVAILABLE` (the driver
27+
error attached as `cause`) instead of proceeding as if the item were unlocked.
28+
Refusing one uncertain write is the intended trade against performing one that
29+
had to be refused. Callers that retry on 503 need no change; callers that
30+
treated a successful save as proof the item was unlocked never had that
31+
guarantee.
32+
33+
The discrimination reuses `rethrowUnlessMetadataStoreUnprovisioned`, introduced
34+
in #5705 for this file's overlay reads, rather than inventing a second
35+
predicate: an unprovisioned `sys_metadata` genuinely has no overlay row, so
36+
`'none'` is the truth and first boot still saves normally; every other error is
37+
an outage.
38+
39+
Unaffected, and covered by regression tests: artifact-level locks (answered from
40+
the in-memory registry before the overlay read is reached), a genuine miss on a
41+
healthy store (still allowed), and control-plane kernels (`environmentId`
42+
undefined), which never enter either gate.

0 commit comments

Comments
 (0)