Stamped Fields

Some values on a node are not data a person enters but facts the node type's own code records — who confirmed an eval case, who approved a request — and something downstream trusts them as an identity. A nightly eval runner, for example, starts each case's conversations as the person in its confirmedBy field. The type's own hub can stamp that field correctly, but as long as anyone holding Update on the node may patch the stored value afterwards, the stamp proves nothing.

So the platform has a rule for it: a stamped field is written by the node's own hub and by nothing else.

Declaring one

A NodeType lists the fields in its definition — the JSON property names inside content:

"content": {
  "$type": "NodeTypeDefinition",
  "configuration": "config => config.WithContentType<EvalCaseContent>().StampConfirmationOnActivation()",
  "stampedFields": ["confirmedBy"]
}

NodeTypeDefinition.StampedFields is declared, not derived: it is read from the type's static registration or its durable NodeType row, the same row that names its sources and configuration. It says nothing about how the value is computed — that stays the type's own code, typically a watcher on its own node that writes the field through GetMeshNodeStream().Update(...).

What the platform enforces

Write Where it is judged Outcome when it touches a stamped field
A cross-hub patch — GetMeshNodeStream(path).Update(...) from any other hub, MCP update / patch / edit_content, the update branch of an upsert, a restore, a GitSync import a delivery step on the node's own hub, before the merge runs refused when the patch would CHANGE the field (a different value, a removal, a splice); naming it re-asserting the live value is a no-op and passes
A data change from another address (a synced subscriber's write-back, a direct data change) updating the node StampedFieldDataValidator, a validator in the data layer's change pipeline on the same hub refused (a failed DataChangeResponse naming the field) when the resulting content differs in the field
A patch of the node hub's data through any reference OTHER than the node itself (a JsonPointerReference, an entity or collection reference) the same delivery step refused on a type that declares stamped fields, naming the field and the reference: its shape cannot be compared field by field, and no platform caller patches a node that way
A DataChangeRequest creation entry for the node's OWN, existing node (the change pipeline applies it as a replace) StampedFieldDataValidator, judged as an update refused when the resulting content differs in the field — including a removal
A create (CreateNodeRequest, an upsert's create branch, a DataChangeRequest creation) StampedFieldCreateValidator, a create validator refused when the content carries a non-empty value for the field
The node's own hub writing its own node (GetMeshNodeStream().Update(...) on its own workspace) — allowed: this is the only path that sets the field
Any read — unchanged

A refused patch is a DeliveryFailure with ErrorType.Unauthorized whose message names the field, the node and the type, for example:

Access denied: the write to 'confirmedBy' on 'Feedback/_Evals/agent/case-1' was refused — 'confirmedBy' is a stamped field of Feedback/EvalCase, written only by the node's own hub, never by a client patch. Nothing was changed; leave the field out of the write.

The whole write is refused, never stripped of the field: a silent strip would tell the writer its value landed. A write that leaves the field alone is unaffected — the check compares the field and nothing else.

Why the owning hub is exact, and the sender is the test

A per-node hub's own writes never travel as a message: GetMeshNodeStream() on its own node commits directly on its data source (MeshNodeStreamHandle.UpdateOwn). Every write from anywhere else — a portal, an agent, another node's hub, an MCP call, even code running as System on another hub — arrives at the node's hub as a PatchDataRequest or a DataChangeRequest whose sender is a different address. So "came from another address" is exactly "not the node's own code", without any identity, role or impersonation having to be trusted. System is not exempt: platform code that runs elsewhere is still a client of the node.

The sender is trustworthy because the posting hub stamps it: a sender the posting code wrote itself is replaced by the address of the hub that actually posts, so naming the node's own address does not make a client the owner (The Sender Is the Posting Hub, #6553).

What it costs

Consequences to know about

Clear on retype

A node hub is bound to the type its node had when it activated, and judges every client write against THAT type's declaration. A retype — a client patch, UpdateNode or data change that changes nodeType — is such a write: it lands on the activation bound to the OLD type, and is never held back or refused for the new type's sake (that would need the new type's declaration on the write path, a row read for a type declared in mesh content).

What follows is the platform's rebind (NodeTypeRebindWatcher): the retiring activation sees the retype post-commit and recycles itself, so the next access binds the new type. Before that recycle it SETTLES the retype (INodeRetypeSettlement, implemented by StampedFieldDeclarationHolder):

  1. It reads the declaration of the type the node carries NOW — the same read a hub start makes, off the write path.
  2. From then on its guard and data validator judge every client write against the NEW declaration, and refuse a further change of nodeType as retryable (the retry reaches the new activation, which settles that retype in turn).
  3. When the new type declares stamped fields, every one that carries a value is removed from the content by the hub's OWN write (as System; it never travels as a message, so no guard judges it) and flushed durably (IPostCommitFlush) — a value written in between, judged against the old declaration, is cleared with it.
  4. Only then does the watcher post the recycle. The activation bound to the new type reads the node back from storage, cleared, and its own code is the only thing that can stamp it again.

A node retyped back to the type its activation was bound to is left alone: every write in between was judged against that very declaration. A retype into a type that declares nothing clears nothing. When the read or the clear faults, the activation keeps a faulted declaration and does NOT recycle: every client write is refused as Unavailable, and the first refusal re-runs the settlement, which recycles the hub itself once it lands — the new type never receives a node its settlement did not finish.

The `sync/

A DataChangeRequest addressed directly to one of the node hub's hosted sync/{id} stream hubs (rather than to the node's own address) is handled there by SynchronizationStream's stream handler, which this guard does not sit in front of. It reaches no data. The sync hub cannot resolve a workspace of its own (An exception was thrown while activating MeshWeaver.Data.IWorkspace), so every data change sent that way is answered with a DeliveryFailure. That includes a change that leaves the stamped field alone, which is how the test tells "the route applies nothing" apart from "the guard happened to fire". The framework's own subscribers never send their write-back there: they address the owner. Test: DataChange_SentToTheOwnersSyncHub_CannotChangeAStampedField. If that handler is ever made to apply changes, it must apply them through the owner, where the guard and StampedFieldDataValidator sit, and that test will fail on the unrelated change first.

Not established

Trusting a stamped field

The rule guarantees who WROTE the field, not that the value is still a valid authority. A consumer that acts on it still re-checks at the time it acts — the authorize as the caller, execute as System pattern: for the nightly runner, that the stamped confirmer still holds Update on the case immediately before the run.

Where it lives