Problem/Motivation

The write-precondition contract (issue #3616541) refuses a governed mutation when MCP Sentinel's own lock table has an active row held by a different principal. Human editors on a typical site do not write that table. They acquire a lock in the contrib Content Lock module by opening the entity edit form.

Those two tables do not talk. A human sitting on /node/N/edit does not create an mcp_sentinel_content_locks row, and McpWritePreconditions does not consult the contrib service. A governed JSON:API PATCH (or Tool-API write) of that node therefore succeeds. Contrib Content Lock itself only disables the form and forbids delete; it does not block entity-API updates, so JSON:API is not saved by that module either.

The documented intent is that locks protect humans from agents. On a site that uses contrib Content Lock for editorial concurrency, that intent is currently unmet.

Steps to reproduce

  1. Enable contrib Content Lock and MCP Sentinel. Configure Content Lock for a node type.
  2. As a human editor, open /node/N/edit and leave the form open (contrib lock held).
  3. As a governed agent (OAuth client credentials or any other governed channel), PATCH the same node over JSON:API.

Expected: 422 content_lock_conflict, same refusal as a Sentinel lock held by another actor. Actual: the PATCH succeeds.

Proposed resolution

When contrib Content Lock is installed, McpContentLock::conflictsForActor() (and therefore every governed write/delete channel) also consults that module's service. An active contrib lock held by a uid other than the acting principal is the same conflict as a Sentinel lock held by another actor. The acting principal's own contrib lock does not block it. Ungoverned (cookie-session) traffic stays ungated.

Content Lock remains optional: no new hard dependency. The contrib service is injected with the optional-service syntax and is a no-op when the module is absent.

Do not honour the "break content lock" permission on the governed path. That permission is for humans in the UI; letting an agent skip a human's form lock would invert the trust model.

Remaining tasks

  • Consult contrib Content Lock in the shared lock service.
  • Kernel coverage: other-uid denies, same-uid passes, ungoverned ignores, expired contrib lock passes, module-absent is a no-op.
  • Document the optional integration.

User interface changes

None.

API changes

None for callers. McpContentLock::conflictsForActor() / isLocked() gain an optional editorial source when the module is present. Optional constructor argument; existing three-argument construction in unit tests remains valid.

Data model changes

None. Sentinel continues to own mcp_sentinel_content_locks; contrib continues to own content_lock.

Comments

jmcerda created an issue. See original summary.

jmcerda’s picture

Status: Active » Fixed

Committed to 1.x. Governed writes now consult contrib Content Lock when that module is installed: an active form lock held by a different uid is the same content_lock_conflict as a Sentinel lock held by another actor. The acting principal's own lock does not block it; ungoverned traffic stays ungated; break-content-lock is not honoured on the governed path. Credit this nid in the next release notes.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

  • jmcerda committed 612baf5f on 1.x
    Issue #3622400: Honour contrib Content Lock on governed writes.
    
    A human...