Core — Write-safety contract¶
Runtime-agnostic contract for mutating the Rosetta graph through the MCP
surface. The rosetta-agent is the live agent: unlike the read-only
engagement-rehearsal plugin (ADR-PA-011), it genuinely writes — engagement
twin state, engagement lifecycle, governance-gate transitions. This document is
the discipline every write skill (engagement-twin, governance-lifecycle,
engagement-lifecycle, engagement-canvas, decision-trace-capture) MUST
honor. Authorized by ADR-RCP-009 (E7-S8-WP1).
For the tool catalog see core/rosetta-tool-vocabulary.md (§3 writes, §5
idempotency, §6 enums, §7 tool-policy). For auth see core/mcp-token.md; for
write provenance reporting see core/provenance.md.
1. Confirm-before-destructive¶
Some writes are irreversible or move governance state backward. Before issuing one, the agent MUST surface the intended write (the tool, the target uid, and a human-readable diff/summary) and wait for explicit operator confirmation in the turn. Do NOT auto-fire these.
Destructive / governance-state class (confirm first):
- rosetta_rollback_a_d_r — reverts an ADR.
- rosetta_reject_governance_review — sends a candidate back / closes a review.
- rosetta_request_governance_review_revision — changes review state.
- rosetta_apply_engagement_twin edits that delete or replace nodes
(an op that removes/overwrites existing twin state); pure additive applies
are non-destructive.
- rosetta_approve_governance_review — irreversible commit of an ADR; confirm.
- rosetta_update_engagement with a clientUid that unlinks (empty string
"" — removes the (Client)-[:HAS_ENGAGEMENT]-> edge + eng.clientUid) or
moves the edge off an existing client — re-parents/removes established
linkage, and an unlinked engagement recreates the client-less state the
INV-D38 load gate exists to prevent. Semantics in
core/engagement-lifecycle.md §Update (app-server
dt-20260706-engagement-client-link-update-dual-write-gap.md, RATIFIED).
rosetta_update_engagementcarries no destructive stage lever:stageis derived, not a settable patch field (seecore/engagement-lifecycle.md/ DT-RX-011 Q-A, RATIFIED, Option 2). Itsname/statuspatches — and aclientUidset where no link yet exists — are additive, non-destructive; the only confirm-before case is theclientUidunlink / edge-move above.
Non-destructive class (proceed, then report): creates and additive updates —
rosetta_create_engagement, rosetta_create_project,
rosetta_create_promotion_candidate, rosetta_create_governance_review,
rosetta_run_gate1, rosetta_evaluate_decision_trace,
rosetta_create_decision_trace/rosetta_record, additive
rosetta_apply_engagement_twin / rosetta_create_digital_twin_node,
rosetta_upsert_practitioner, rosetta_update_engagement (except the
clientUid unlink / edge-move case above). Issue these, then report what was
written (§5).
REJECTED: a destructive write with no prior operator confirmation in the conversation. REJECTED: hiding a destructive write inside a batch the operator only approved at a high level — name each destructive op.
1.5 Content-honesty / provenance routing (ADR-RX-017 Q-C)¶
The forensic twin (rosetta_apply_engagement_twin) and the rehearsal twin
(rosetta_apply_engagement_twin_rehearsal, core/rosetta-tool-vocabulary.md §3)
are two lanes, distinguished by the honesty of the content being written,
not by the operator's permission mode. Route by what the payload claims:
- Simulated / proposed content MUST route to the rehearsal lane. A write
whose content carries any simulated marker —
provenance:"simulated", a[SIMULATED]label,status:PROPOSED, or asimulationRunId— MUST go torosetta_apply_engagement_twin_rehearsal. That lane admitstraceKind:"runtime"edits and stamps the revision ProvenanceEventprovenance:"simulated", so the simulated origin is durably recorded. Sending such content to the forensicrosetta_apply_engagement_twinis REJECTED (the forensic lane is for confirmed graph-canonical state). - Presenting simulated values as confirmed is misrepresentation — REJECTED.
An apply that carries simulated/proposed values but strips the markers (no
[SIMULATED],provenance:"confirmed", nosimulationRunId) and routes them through the forensic lane as if a human ratified them is REJECTED as misrepresentation. Do not launder a rehearsal proposal into a confirmed claim by deleting its provenance. - The honest test-fill workflow now has a home. Rehearsal→fill→render-test is
legitimate and recurring (the engagement-rehearsal
proposed-twin-apply.jsonloop,core/engagement-twin.md§6). It writes honestly-labelled simulated content to the rehearsal lane; it never re-routes through direct bolt to dodge the forensic guard, and it never drops the markers to satisfy a confirmed-only reader.
Known limitation — agent-self-attested, NOT server-enforced honesty. §1.5 is a heuristic keyed on labels the agent itself writes: a misrepresenting agent can still omit the marker and route to the forensic lane, and the server will accept it. This rule improves intent-recognition and gives honest workflows a reachable lane; it cannot make honesty server-verifiable. The stronger, server-enforced attestation that DT-RX-017 §Ratification deferred ("Server-enforced honesty — deferred") now exists for the ratification act itself: DT-RX-022 put the lane-flipping verb server-side and out of agent reach entirely (§1.6). The apply lanes remain agent-self-attested — document the limitation; do not pretend the §1.5 guard is tamper-proof.
1.6 Ratification is human-only — the agent surface has no such verb (DT-RX-022 Q-D)¶
Ratifying or rejecting engagement-twin content — flipping reviewStatus to
human-ratified/rejected, promoting provenance simulated → confirmed,
clearing simulationRunId — is the human gate of the provenance lane, and it
is enforced structurally, not by this contract's discipline:
- The rosetta MCP surface deliberately has no ratify/reject tool. The
server-side
twin:ratify/twin:rejectendpoints (DT-RX-022 Q-A) exist on REST for the enablement console's authenticated human sessions only; the mcp-server does not wrap them (theensureAccountForUserprecedent — excluded under a denied tag, same posture as §6's denied families).reviewedByaccepts only a human identity; the server rejects agent-principal ratifications. - Ratify/reject are HUMAN-ONLY via the enablement console — the
enablement-agent review queue in the
(protected)/engagementspages. When a task appears to need a ratification (a customer-tone render blocked onagent-authorednodes, a simulated run awaiting confirmation), the agent surfaces it as an operator action and STOPS — exactly the §6.1 out-of-band posture. - Agents propose via the simulated lane and STOP. The agent's whole
authority on this axis is §1.5: write honestly-labelled simulated/proposed
content to the rehearsal lane and report it for review. Do NOT hand-write
reviewStatus: "human-ratified",reviewStatus: "rejected", or aprovenance: "confirmed"promotion onto existing lane-stamped nodes through the apply paths to imitate a ratification — that is the §1.5 misrepresentation class, and the transition is the server's to make (one-way state machine:draft/agent-authored→human-ratified|rejected, both terminal; metamodel v1.11.0 §0.7).
REJECTED: any attempt to reach twin:ratify/twin:reject via direct REST
(RCP-C-6 direct-bolt boundary, §6.1). REJECTED: prompting the operator for a
blanket "yes" and then simulating the ratification with apply-path writes.
2. Idempotency by default¶
- Reuse the natural keys in
core/rosetta-tool-vocabulary.md§5; never mint a freshsessionId/decisionPointto force a duplicate write. - For
rosetta_apply_engagement_twin, ALWAYS send anIdempotency-Key(a uuid pinned for the logical apply) so a retry is safe. - Treat 200-where-you-expected-201 as a normal idempotent hit, not an error.
- 409 Conflict → state moved under you: reload (
rosetta_get_engagement_twinwithsince=, or the relevant get) and re-evaluate; do NOT blind-retry. - 422 Unprocessable → referential failure (a referenced uid does not exist / FK violation): surface it, fix the reference, retry.
3. Enum / validation are hard¶
The server rejects off-enum or malformed writes with 400. Pre-validate every
enum field against core/rosetta-tool-vocabulary.md §6 BEFORE the call
(decisionType, outcome, retrievalStrategy, surfacedBy, phase,
solutionMode, overrideMode). A 400 is a client bug, not a transient fault —
fix the payload, do not retry unchanged.
clientUid is required at engagement birth (api 1.5.0, BREAKING).
rosetta_create_engagement with clientUid absent, empty, or off the
:Client.uid slug shape (^[A-Za-z0-9_.:\-]+$, ≤64) is rejected 422 before
any write — same fix-the-payload posture as a 400; do not retry unchanged and
do not fabricate a client slug: resolve the real client (or confirm the
intended new one — the Client node is lazily MERGEd, so a typo silently
mints a new client) BEFORE the call. Contract detail in
core/engagement-lifecycle.md §Create.
4. Partition / access¶
Engagement writes require the bearer to have access to the engagement (the
upstream REST server authorizes every call). 401 = missing/invalid bearer
(config error per core/mcp-token.md; do not retry with a different token).
403 = the bearer lacks access to this engagement (a partition/ACL error —
surface it; engagement-access grant/revoke is a denied tag, an out-of-band
operator action, never a plugin call). Neither is a retryable fault.
5. Write provenance (RCP-C-4)¶
Every write is reported back to the operator: the tool called, the resulting
uid, and the idempotency outcome (created / existed / conflicted). For a
twin apply, report the applied edits[] count and the returned revision. The
operator must always be able to see exactly what was mutated and where — writes
are never silent.
6. The tool-policy boundary¶
The server's deny-by-default allowlist (core/rosetta-tool-vocabulary.md §7) is
the outer bound of what the agent may write. Denied families (accounts,
platform-admin, engagement-access, export, presence, project fork) are NOT
plugin capabilities; if a task seems to need one, it is an operator / out-of-band
action. Never hand-roll a denied endpoint or raw /api/v1/* (RCP-C-6).
6.1 The direct-bolt boundary (RCP-C-6) and the recovery-cleanup class¶
The agent's only sanctioned write path is the MCP surface (RCP-C-6). A direct
bolt write to Neo4j — bypassing the MCP server to issue raw Cypher against the
graph — is never a rosetta-agent action. This is the same posture as the
denied engagement-access family: when a task appears to need it, that signals an
operator / out-of-band step, not a capability the agent reaches for.
There is exactly one legitimate direct-bolt class, and it is not an agent
capability: owner-authorized recovery / cleanup (e.g. a one-time
simulationRunId purge, or repairing graph state a prior incident contaminated).
This is an explicit out-of-band operator action, named here so the honesty
guard (§1.5) stops depending on permission-mode toggling. The agent does not
flip its own permission mode to "earn" a direct bolt; if recovery/cleanup is
needed, the agent surfaces it as an owner action and stops. (This codifies the
DT-RX-017 §7 rows-2-vs-5-7 oscillation — where honest simulated writes were
re-routed through direct bolt and unblocked only by removing auto-mode — into a
named boundary rather than a mode toggle.)
7. Pre-flight for writes¶
Before the first write of a turn, run the write pre-flight in core/preflight.md
(token presence + reachability + engagement-access precondition + Idempotency-Key
generation for twin apply + the confirm-before-destructive gate above).
Consumed by¶
skills/engagement-twin/SKILL.mdskills/governance-lifecycle/SKILL.mdskills/engagement-lifecycle/SKILL.mdskills/engagement-canvas/SKILL.mdskills/decision-trace-capture/SKILL.mdcommands/{engagement,twin,govern,canvas}.md