Field access can't see the incoming value: govern Drupal writes with a validation constraint
We govern what AI agents may do to a Drupal site — which entities they may touch, whether they may publish, and (new this week) whether they may point a redirect off-domain. The obvious hook for "an agent may not set field X to value Y" is field access. It is the wrong hook, and the reason is subtle enough to cost you a release.
The trap: field access checks the stored value
On a JSON:API or REST write, Drupal's EntityResource checks field edit-access against the entity's original, stored value — not the value the request is trying to set. So a hook_entity_field_access() gate that reads the field's current value sees what is already in the database, never the incoming target.
We hit this with a publish gate. The rule was "an agent may not move content to published." Implemented as field access, it read the node's current moderation_state — already published for a live page — and denied every write to that field, including the legitimate published to draft transition an agent needs to stage an edit. The gate saw the wrong value and blocked the wrong thing.
The seam: a validation constraint
Validation runs on the parsed entity — the one carrying the incoming values — before it is saved, on every JSON:API, REST, and form write. That is exactly the layer that can compare what is being set against policy.
// Attached to the entity type; runs on the incoming values
public function validate($entity, $constraint) {
if (!$this->governed()) return; // only govern agent traffic
$target = $entity->get('redirect_redirect')->uri;
if ($this->isExternal($target) && !$this->allowed($target)) {
$this->context->buildViolation($constraint->message)
->atPath('redirect_redirect')->addViolation();
}
}A denied write returns a clean 422 with the offending field named, instead of a confusing 403 from an access check that was reasoning about stale data.
Why it is the right layer for agent governance
- It sees intent. The incoming redirect target, the target moderation state — the thing you actually want to allow or deny.
- It is uniform. One constraint covers JSON:API, REST, and the admin form. No per-transport gate.
- It fails closed and explains itself. A violation is a specific, path-anchored message, not a blanket denial.
- It is cheap to scope. Early-return for non-agent traffic, and attach the constraint only when the relevant module is installed, so ordinary sites are untouched.
The rule of thumb
If your policy depends on what the request is trying to set, put it in a validation constraint. Field access answers "who may touch this field," judged against what is already there — not "what may this write contain."