Skip to content

The package permission door deletes other packages' rows when the save-time layered re-read fails #9420

Description

@os-tesla

What

PermissionMatrixEditPage.doSave, under the package door, is documented to merge
the edited slice back onto a fresh read so that other packages' contributions
survive. Its own comment states the guarantee:

Package scope: merge only this package's slice back onto a fresh read of the
record so rows contributed by other packages survive byte-for-byte (ADR-0086 P0).

That fresh read is client.layered(...).catch(() => null). When it fails, the
guarantee inverts
: the merge base falls back to payload — the editor's own
draft, which the load path already sliced down to this package's objects — so
mergePermissionSlice has no out-of-scope rows left to copy and the save PUTs a
record with the other packages' rows deleted. Success, 200, no error, no warning.

const fresh = await client.layered(type, payload.name).catch(() => null);
const base = (fresh?.effective ?? payload);   // THE INVERSION
toSave = mergePermissionSlice(base, payload, scope);

Measured, not reasoned

One-shot probe driving the real PermissionMatrixEditPage (deleted after the run;
it was not landed). Published record carries a_account plus b_order (another
package's contribution) and the field permission b_order.total. The package door
is opened as app.a, whose object list is a_account only. layered resolves for
the load and rejects for the save-time re-read. One ordinary edit, then Save:

PROBE layeredCalls        = 2
PROBE saved.objects keys  = ["a_account"]
PROBE saved.fields keys   = []
PROBE b_order present     = false

b_order and b_order.total are gone from the body actually handed to
client.save. On the success arm the same scenario keeps them — that arm is pinned
today by PermissionMatrixEditor.packageDoorFacets.test.tsx. Only the failure arm
is unpinned, and only the failure arm loses rows.

Why it is worth a card

This is the same silent-discard class as objectui#4302 (an authored RLS policy
dropped with a 200), which is closed and does not cover this arm — its pins all
run with layered resolving. The blast radius here is larger than #4302's: #4302
lost the author's own unsaved edit, this loses another package's already-published
grants
, and permission rows are a security surface.

⚠️ Not measured, and stated rather than assumed: how often layered actually
rejects in production. The arm is reachable by construction and the probe reaches
it, but no field incident is attached to this card.

Suggested shape, not a decision

The cheapest correct behaviour is probably to refuse the save when the re-read
fails, rather than proceed on a base that cannot honour the guarantee — a failed
re-read means the information needed to preserve out-of-scope rows is simply not in
hand. Whether that is a hard error, a retry, or a narrowed save is a product call
and is deliberately left open here.

⛔ Note that "merge from the unsliced draft instead" is not a fix: the load path
slices deliberately (ADR-0086 P0 keeps other packages' rows off-screen), so the
unsliced body is not available at that point either.

Provenance

Found while working objectui#8181 (PR #9419), whose pin harness exercises this exact
arm for a different reason (read decorations). Fenced out of that PR rather than
fixed there: different defect class, and #8181's fence is the read-decoration strip.
Deduped against the 63 open-and-closed issues carrying package: app-shell
mergePermissionSlice, ADR-0086, and byte-for-byte row-preservation phrasings
return zero hits; the control term draft-envelope returns objectui#8181, so the
grep shape does reach the population.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: app-shellpriority:p1

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions