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_digital_twin_node / rosetta_update_digital_twin_node_by_key
patches that remove or overwrite an existing property
(core/engagement-twin.md §4).
- rosetta_ingest_content without dryRun: true (a commit) — and only against
a non-production server; against production it is REJECTED until the
DT-RX-036 switch (§1.7).
- 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).
On a platform admin's key this rule is the only interlock. The governance decision verbs and canonical node writes stay on the agent surface, and the upstream admits them for a platform admin (app-server
gate3-governance-authz.mdQ-C-2;pre-switch-write-lane-authz.mdQ-B-1). Confirm-before is then a client-side courtesy, not a control — honor it every time.
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), and rosetta_ingest_content with
dryRun: true (it writes nothing). 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§3.5). 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.
1.7 Content ingest — dry runs only before the switch (rosetta-platform E9-I1)¶
rosetta_ingest_content (mcp v0.11.0) lands a plugin run's canonical bundle in
the live graph in one transaction, and it needs a platform admin's key (403
otherwise). Until the DT-RX-036 switch (rosetta-platform manifest §11B,
E9-I1), the seeds are the content authority and no plugin submits to the
production ingestion API:
- Against production, send
dryRun: trueonly. A dry run validates the whole bundle and returns the plan (status: planned, 200) without writing. The plugin's defaultROSETTA_API_BASEis production. - A commit (
dryRunabsent or false) is for a non-production server, at the operator's direction, confirm-before (§1). It returns 201 with the IngestionEvent; created patterns landdraft, ADRs and BusinessIntentsproposed. - REJECTED: committing a bundle to production before the switch, whatever the key and whatever the operator's permission mode.
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 → the payload broke a rule: a referenced uid does not
exist / FK violation, a node write that would change the key addressing the
node, a create or patch on an own-lane label, a
baseVersionon an engagement node's patch, or a refused ingest bundle (the body lists every failure). Surface it, fix the payload, retry. - For a patch to a node outside any engagement (canonical content), send
baseVersionfrom your last read so a concurrent edit is a 409, not a silent overwrite. Do not send it for an engagement node: that node keeps time on its engagement revision, and abaseVersionthere is 422.
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, and gapType on a turn projection's gaps, which
also need a description; core/conversation-lifecycle.md §12). 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¶
The upstream REST server authorizes every call against the bearer's user
(core/rosetta-tool-vocabulary.md §7.1). 401 = missing/invalid bearer
(config error per core/mcp-token.md; do not retry with a different token).
403 has two readings:
- On a governance tool (reads included), a canonical node write, any node
create, or
rosetta_ingest_content: this key isn't a platform admin. Say so plainly and stop. Platform admin is granted out of band by an existing platform admin; theplatform-admintag is denied on this surface. - On an engagement-scoped call: the bearer has no grant on that engagement, or, for a write, is not its lead or the account's owner/admin (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 401 nor 403 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
This page as Markdown — for pasting into Claude or another assistant · llms.txt