You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Move re-stamps createdBy/createdDate/lastModifiedBy on the moved node and its whole subtree — a relocation is recorded as a creation, destroying authorship #3263
Move (menu item, and the move MCP op) re-stamps the moved node's authorship and creation metadata as if the node had just been created by the person doing the move. createdDate, createdBy, lastModified and lastModifiedBy are all overwritten; only version survives.
This contradicts the field's own contract in MeshNode:
createdDate — "Timestamp when this node was first created. Set once at creation time; never updated thereafter." createdBy — "Identity … that created this node. Stamped at creation time and never changed."
A move is a relocation. It is not a creation and not a content edit, so it should preserve all four stamps.
Measured — memex.systemorph.com, 2026-09-03
ClientA/EslProposal/Pricing was authored by a colleague and last edited by them on 2026-09-01. It was moved (with its parent subtree) to ClientA/Esl/EslProposal/Pricing at 20:15:43Z. Reading it back immediately after:
{
"path": "ClientA/Esl/EslProposal/Pricing",
"createdDate": "2026-09-03T20:15:43.521686+00:00", // ← the move"createdBy": "rbuergi", // ← whoever ran the move"lastModified": "2026-09-03T20:15:43.491882+00:00", // ← the move"lastModifiedBy": "rbuergi", // ← whoever ran the move"version": 47// ← preserved
}
The node now claims it was created today by the person who moved it. The real author and the real creation date are gone from the node.
version: 47 surviving is the tell: this is not a copy-and-delete losing identity, it is the same row with its stamps rewritten.
Why this is serious rather than cosmetic
Nothing in the product recovers the lost values, because there is no version history to fall back on (filed separately — the two together are what make this unrecoverable rather than merely annoying). So a single subtree move silently and permanently rewrites the authorship of every node under it.
Blast radius scales with the subtree: one restructure of a client space rewrote the stamps on ~80 nodes in one action, including decks, slides and a commercial proposal, all of which now read as created that minute by the person who ran the move. After the fact there is no way to tell who actually wrote any of them, or to distinguish a node that was merely relocated from one that was genuinely authored that day.
Expected
Move preserves createdDate, createdBy, lastModified and lastModifiedBy on the moved node and on every descendant. If a move should be recorded at all, it belongs in the activity log or as its own history entry — not by overwriting who wrote the thing.
Repro
Create a node as user A; note its createdBy / createdDate / lastModifiedBy.
As user B, move it (or an ancestor of it) to another path.
get the node at its new path — all four stamps now name user B and the moment of the move.
Applies to the whole subtree, not just the node named in the move.
Cross-reference: #3264 — no version history is retained on this portal, so the stamps this bug overwrites cannot be recovered afterwards. Independent root causes; together they turn a Move into permanent loss of authorship.
Fixed on main by #3273 (merged 2026-09-04). Closing by hand — core's merge queue does not fire Closes links; six issues behaved that way today.
🚨 Verified by reading the merged source, and existence-checked first — earlier today I closed three issues on a git show <ref>:<path> | grep … || echo clean that printed "clean" because the path did not exist. So: git cat-file -e origin/main:src/MeshWeaver.Mesh.Contract/MeshExtensions.cs → exists, then read it.
The mechanism was TWO halves, and the issue's "Expected" only named the symptom
Half 1 — the copy leg re-stamped unconditionally.Move is copy-to-target + delete-the-source, and RetargetNode stamped LastModified = UtcNow on every copied node — so the moved node and every descendant got it.
Half 2 — and this is the one nobody could have guessed from the issue. The copy leg re-created its nodes from meshService.Query<MeshNode>("path:{source} scope:subtree") — a projection, not the stored node. PostgreSqlSqlGenerator.GenerateSelectQuery selects id, namespace, name, node_type, description, category, icon, display_order, last_modified, version, state, content, desired_id, main_node, sync_behavior, exclude_from_context — and notcreated_by, created_date, last_modified_by. Those are real columns the point read and the INSERT both use, but no query projects them, and PgMeshNodeReader returns null for an absent column by design. So on Postgres every node arrived with blank stamps and HandleCreateNodeRequest filled them with "now, by the caller".
That is why version: 47 survived while the four stamps did not — it confirms the row is the same row, and it also explains why this could never be reproduced on the in-memory provider, which returns the stored node verbatim. Filed separately as Systemorph/MeshWeaver.Plugins#1310.
What landed
RetargetNode now takes preserveAuthorship, and a move passes true:
returnpreserveAuthorship?relocated// Id / Namespace / MainNode change; the four stamps ride along:relocatedwith{CreatedDate=default,CreatedBy=null,LastModified=default,LastModifiedBy=null};
and the copy leg reads each node from IStorageAdapter rather than trusting the query projection — the query stays the RLS-filtered decision of which nodes, storage answers what they are. No values are written back after the fact.
Copy deliberately does the opposite, and that distinction is the reason this is a flag rather than a blanket rule: AccessContextScope impersonates MeshNode.CreatedBy, so a copy that inherited the original creator would run owner-scoped work as someone who never touched the node.
A privilege-escalation hole found and closed while gating this
Measured, not theorised: with the flag ungated, an Editor (Read | Create | Update, no Delete) could copy an admin's node with authorship intact — minting a node in their own space that the platform then impersonates the admin for. RLS does not close it, because reading the source is exactly what an Editor may do. PreserveAuthorship now requires Delete on the source namespace — the same entitlement MoveNodePermissionAttribute requires of a mover — with UNDETERMINED refusing as unavailability rather than denial.
Test
test/MeshWeaver.Hosting.Test/MoveNodePreservesAuthorshipTest.cs, over the real MonolithMeshTestBase, asserting the moved node and a descendant. Without the fix it failed on the timestamps (lastModified …39.3262830Z → …39.4102920Z on the root, …39.3278600Z → …39.4127360Z on the descendant); with it, 2/2. The copy control arm fails on main without the change too.
Honest limit recorded in the PR: the projection half is not falsifiable by a core test, because the in-memory provider returns the stored node verbatim. Its evidence is the Postgres SELECT list.
#3264 (no version history retained) remains the reason this was unrecoverable rather than merely wrong — the reporting half of that is #3278; the store itself is still open.
Activity
rbuergi commentedon Sep 3, 2026
Cross-reference: #3264 — no version history is retained on this portal, so the stamps this bug overwrites cannot be recovered afterwards. Independent root causes; together they turn a Move into permanent loss of authorship.
rbuergi commentedon Sep 4, 2026
Fixed on
mainby #3273 (merged 2026-09-04). Closing by hand — core's merge queue does not fireCloseslinks; six issues behaved that way today.🚨 Verified by reading the merged source, and existence-checked first — earlier today I closed three issues on a
git show <ref>:<path> | grep … || echo cleanthat printed "clean" because the path did not exist. So:git cat-file -e origin/main:src/MeshWeaver.Mesh.Contract/MeshExtensions.cs→ exists, then read it.The mechanism was TWO halves, and the issue's "Expected" only named the symptom
Half 1 — the copy leg re-stamped unconditionally.
Moveis copy-to-target + delete-the-source, andRetargetNodestampedLastModified = UtcNowon every copied node — so the moved node and every descendant got it.Half 2 — and this is the one nobody could have guessed from the issue. The copy leg re-created its nodes from
meshService.Query<MeshNode>("path:{source} scope:subtree")— a projection, not the stored node.PostgreSqlSqlGenerator.GenerateSelectQueryselectsid, namespace, name, node_type, description, category, icon, display_order, last_modified, version, state, content, desired_id, main_node, sync_behavior, exclude_from_context— and notcreated_by,created_date,last_modified_by. Those are real columns the point read and the INSERT both use, but no query projects them, andPgMeshNodeReaderreturns null for an absent column by design. So on Postgres every node arrived with blank stamps andHandleCreateNodeRequestfilled them with "now, by the caller".That is why
version: 47survived while the four stamps did not — it confirms the row is the same row, and it also explains why this could never be reproduced on the in-memory provider, which returns the stored node verbatim. Filed separately as Systemorph/MeshWeaver.Plugins#1310.What landed
RetargetNodenow takespreserveAuthorship, and a move passestrue:and the copy leg reads each node from
IStorageAdapterrather than trusting the query projection — the query stays the RLS-filtered decision of which nodes, storage answers what they are. No values are written back after the fact.Copy deliberately does the opposite, and that distinction is the reason this is a flag rather than a blanket rule:
AccessContextScopeimpersonatesMeshNode.CreatedBy, so a copy that inherited the original creator would run owner-scoped work as someone who never touched the node.A privilege-escalation hole found and closed while gating this
Measured, not theorised: with the flag ungated, an Editor (
Read | Create | Update, noDelete) could copy an admin's node with authorship intact — minting a node in their own space that the platform then impersonates the admin for. RLS does not close it, because reading the source is exactly what an Editor may do.PreserveAuthorshipnow requiresDeleteon the source namespace — the same entitlementMoveNodePermissionAttributerequires of a mover — withUNDETERMINEDrefusing as unavailability rather than denial.Test
test/MeshWeaver.Hosting.Test/MoveNodePreservesAuthorshipTest.cs, over the realMonolithMeshTestBase, asserting the moved node and a descendant. Without the fix it failed on the timestamps (lastModified …39.3262830Z → …39.4102920Zon the root,…39.3278600Z → …39.4127360Zon the descendant); with it, 2/2. The copy control arm fails onmainwithout the change too.Honest limit recorded in the PR: the projection half is not falsifiable by a core test, because the in-memory provider returns the stored node verbatim. Its evidence is the Postgres SELECT list.
#3264 (no version history retained) remains the reason this was unrecoverable rather than merely wrong — the reporting half of that is #3278; the store itself is still open.
Merge origin/main (#3273's authorship fix) into the satellite fix
fix(access): a move carries authorship by naming its stored source, g…