Skip to content

Enforce Core/System field protection on update_schema removes and renames, validate rename grammar before persisting - #2304

Merged
mstomar125 merged 1 commit into
mainfrom
worktree-issue-2194-2195-2208-2213-schema-protection
Aug 27, 2026
Merged

mstomar125 merged 1 commit into
mainfrom
worktree-issue-2194-2195-2208-2213-schema-protection

Conversation

@mstomar125

Copy link
Copy Markdown
Contributor

Summary

Closes #2194, #2195, #2208, #2213 — a real cluster of bugs in update_schema's field-removal/rename path.

  • handle_update_schema's remove_fields deleted schema fields by name alone (fields.retain), with no SchemaProtectionLevel check.
  • NodeService::rename_schema_field and update_schema_field_friendly_name rekeyed/relabeled fields by name alone, also with no protection check — even though SchemaNode::can_delete_field/can_modify_field exist specifically to guard this.

Together, a single update_schema call could permanently remove or rename a Core- or System-protected field (e.g. task.status) on a core schema, migrating/losing every existing node's data with no recovery path through the API (#2194, #2195, #2213).

Separately, rename_fields executed the rename (migrating every node's property data and rewriting the schema) in Phase 1, and only validated the destination name's grammar in Phase 2 — after the migration had already committed. A malformed destination (e.g. containing a space) would be durably persisted to the schema and every node instance before the call reported failure, wedging the schema until manually renamed back (#2208).

Fix

  • Added a protection-level guard in handle_update_schema's Phase 0, checked against the pre-mutation schema snapshot, before any mutation executes: remove_fields and rename_fields (source field) are rejected if the field is not User-protected.
  • Added the same guard directly in the NodeService primitives (rename_schema_field, update_schema_field_friendly_name) as defense-in-depth, so a future second caller can't reintroduce the bypass by skipping the upstream check.
  • Moved rename-destination grammar validation (validate_schema_field_name) into that same Phase 0 pass, before Phase 1 runs, so a malformed name is rejected before any migration or schema rewrite happens — never partially/durably applied.

Tests

New regression tests cover:

  • Core- and System-protected field removal rejected, field survives.
  • User-protected fields remain removable/renameable even on a core schema (field-level, not schema-level guard).
  • A batch with one protected field among several rejects the whole call (no partial application).
  • Identity renames and friendly-name-only relabels of protected fields both rejected.
  • Rename destination grammar validated before persisting — schema and node data verified untouched by a rejected rename, and a subsequent valid update on the same schema still succeeds (schema isn't left wedged).

Test plan

  • bun run test:all — all green (1215/1215 core unit tests, full frontend/skill/rust suite)
  • bun run quality:fix — clean, no warnings
  • Pushed through the real pre-push gate (test:all + nodespaced build + e2e + Tauri-seam tests) — passed

🤖 Generated with Claude Code

…ames, and validate rename grammar before persisting (closes #2194, #2195, #2208, #2213)

handle_update_schema's remove_fields deleted schema fields by name alone,
and NodeService::rename_schema_field/update_schema_field_friendly_name
rekeyed and relabeled fields by name alone — neither consulted
SchemaField::protection despite SchemaNode::can_delete_field/
can_modify_field existing for exactly this. A Core-protected field like
task.status (or any System-protected field) could be permanently removed
or rekeyed via a single update_schema call, corrupting a core schema with
no recovery path through the API.

Add a protection-level guard in two places: handle_update_schema's Phase 0
(before any mutation runs, checked against the pre-mutation schema
snapshot) and the NodeService primitives themselves, so a future second
caller of rename_schema_field/update_schema_field_friendly_name can't
reintroduce the bypass by skipping the upstream check. Only
User-protected fields may be removed, renamed, or relabeled.

Also move rename destination grammar validation
(validate_schema_field_name) into that same Phase 0 pass. Previously a
rename ran in Phase 1 (migrating every node's property data and rewriting
the schema immediately) and grammar was only checked in Phase 2, after
the migration had already committed — so a malformed destination name
(e.g. containing a space) would be durably persisted to the schema and
every node instance before the call reported failure, wedging the schema
until manually renamed back.

Adds regression tests covering: Core- and System-protected field removal
rejection, User-protected fields remaining removable/renameable on core
schemas, whole-batch rejection when one entry is protected, rejection of
both identity renames and friendly-name-only relabels of protected
fields, and the grammar-before-persist ordering (schema and node data
both verified untouched by a rejected rename, with a subsequent valid
update on the same schema still succeeding).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update_schema deletes Core/System-protected schema fields with no protection-level check (guard bypass)

1 participant