Editor Integration Spec — DAON Provenance Versioning
Status: draft for implementers · Companion to: provenance-data-model.md
This is the contract between an editing tool and the DAON versioning layer. It is written so that an editor team can build against it without reading the data model, and without being able to violate it by accident.
The versioning layer is a local library plus a local agent. It is not a network service, it does not require the DAON chain, and no content ever leaves the machine.
1. The one rule
The editor reports what it observed. It never reports what the content is.
An editor cannot know whether text came from a person, a notes app, or a model. Any field claiming to know is either a lie or a guess, and both are worse than silence.
So the API has no source, no is_human, no ai_generated, no confidence score, and no field
that could be reduced to one. Observation.authoritative is hard-coded false — a structural
reminder that tools observe and never adjudicate.
An editor that infers source and reports it is not a conforming client. If a future editor wants to display a guess to its own user, that is its business — but it must not enter the log, because the log is evidence and a guess is not.
2. Trust boundary
┌─ editor process ──────────────┐ ┌─ versioning agent ─────────────────┐
│ │ │ │
│ keystrokes, paste events, │──▶│ coalescing rate limits │
│ cursor, buffer state │ │ leaf construction Merkle append │
│ │ │ author key witness batch │
│ reports observations │ │ local content store │
│ requests commits │◀──│ → OpenTimestamps (the only egress) │
└───────────────────────────────┘ └────────────────────────────────────┘
untrusted for enforces everything
timing, identity, rates that matters
The editor is trusted to report its own observations honestly. It is not trusted for timing, identity, ordering, or restraint. Specifically:
| Concern | Enforced by | Why not the editor |
|---|---|---|
| Rate limiting | agent | a buggy editor in a hot loop must not reach the witness |
| Wall-clock time | agent + beacon | local_time is creator-asserted and explicitly untrusted |
Leaf ordering / seq |
agent | must be monotonic even if the editor crashes or reorders |
| Signing | agent | the author key never enters editor memory |
| Witness cadence | agent | the shared resource; see §5 |
A hostile editor can pollute its own creator’s log with false observations. It cannot forge another creator’s history, backdate a leaf below its beacon, or exhaust the witness on anyone else’s behalf. Those are the properties worth defending.
3. API
Transport — normative
The agent holds the author key. TCP loopback is not a security boundary: any local process, including anything malicious that gains a foothold, can connect to it. There is no listener on any TCP port, loopback included.
| Platform | Transport | Protection |
|---|---|---|
| macOS, Linux | Unix domain socket | file mode 0600, owned by the running user |
| Windows | named pipe | DACL restricted to the current user SID |
HTTP semantics over that socket, so any language with an HTTP client can integrate without an FFI story. Implementations must not offer a TCP fallback, including as a development convenience — a debug flag that opens a port is a debug flag that ships.
A caller that can open the socket is already running as the creator and can read the key material directly; the socket is not trying to defend against that. It is defending against every process that cannot, which on a shared or compromised machine is the population that matters.
What an entity is — normative
An entity is one canonical assembled artifact. The manuscript, not the project; the manuscript, not the chapter file.
The data model defines an entity structurally — identity is its genesis, position is its head — but not what it corresponds to in the world. That is the first decision an implementer faces, and getting it wrong is silently corrosive rather than loud.
Why not one entity per file. Long-form writing is mostly restructuring. Move a scene from
chapter 3 to chapter 7 and, with chapter files as entities, chapter 7 records ingress: paste —
byte-identical to pasting from a model. Split a chapter and you create a fresh genesis with no
lineage at all until forks land in P2.
So an author reorganising their own manuscript would accumulate exactly the pattern §6 warns against reading as suspicious, by doing the ordinary work of writing a book. The system would be manufacturing the signal, then disclaiming it. That is worse than not recording it.
Why the assembled artifact works. Internal restructuring adds no bytes from outside the
entity, so it produces no paste and needs no fork. Which is correct: reorganising your own
work is not an evidentiary event. Bytes crossing the boundary — from another document, another
person, a model — are, and those still register.
Project-level is too coarse for the opposite reason: unrelated works would share a head, so witnessing one would carry the others, and disclosing about one would mean disclosing a head covering all of them.
An editor that stores a work as multiple files opens one session per assembled artifact and
sends the assembled content at commit. How it maps files to that assembly is its business; the
entity boundary is not.
Collections are a non-goal. There is no series, anthology, or body-of-work primitive, and there should not be one.
Provenance is a property of a work. Grouping works is cataloguing, and it is cataloguing with a
cost: a collection is an aggregation surface, and aggregation is where scoring becomes
possible. “Prove this manuscript is yours” and “show me your body of work” are different asks,
and only the first is answerable without characterising a person. A grouping primitive would make
a creator’s whole output legible as a unit, which is the creator profile
provenance-data-model.md refuses to build.
The legitimate need is already met without one: works by the same creator share an author_key.
That link exists for any creator who chooses to reveal it, and exists for nobody else — which is
the correct default. A collection primitive would move that from something disclosed to something
queryable.
Lineage between entities, where one work genuinely derives from another, is what forks are for (P2). That is a provenance relation. Belonging to the same series is not.
Anthologies: the entity model already covers this
The case that looks like it needs collections does not. An anthology is an assembled artifact, which is what an entity is. Who registers it follows from who holds the rights:
| Rights | What is registered | Signed by |
|---|---|---|
| Publisher holds copyright in the collected whole | the anthology, as its own entity | the publisher’s key |
| Author retains rights in their contribution | that contribution, as its own entity | the author’s key |
| Both, which is common | both entities, independently | their respective keys |
The key that signs is the ownership assertion. DAON records who claimed what and never encodes the rights deal itself, which is a negotiated creative and legal choice between the parties — not something a format should presume.
Overlapping entities are correct, not a conflict. When a publisher registers an anthology containing a work the author has also registered, the same content sits under two entities with different keys. That is an accurate representation of the arrangement: two true claims at different scopes. An implementation must not treat overlapping or identical content across entities as duplication, conflict, or a signal of anything. There is nothing to reconcile, and reconciling would mean adjudicating a rights question we have neither the standing nor the information to decide.
POST /v1/session/open
Begin editing an entity. Returns a session handle used for subsequent calls.
// request
{ "entity_id": "sha256:…", // omit to create a new entity (genesis)
"tool_id": "acme-editor/0.4.1" }
// response
{ "session": "s_01J…",
"entity_id":"sha256:…",
"head": "sha256:…",
"head_seq": 41,
"limits": { "min_commit_interval_ms": 2000,
"daily_leaf_budget": 2000,
"leaves_remaining_today": 1974 } }
The agent returns its live limits so the editor can pace itself rather than discover them by being refused. Clients must read these rather than hard-coding the defaults in §5.
POST /v1/observe
Report authoring activity. Cheap, frequent, non-committing. The agent accumulates observations and decides when they become a leaf.
{ "session": "s_01J…",
"ingress": "keystroke_stream", // | paste | import | programmatic | unknown
"span_bytes": { "added": 214, "removed": 12 },
"op_count": 87,
"duration_ms": 45200 }
ingress must reflect the mechanism observed, not an inference about origin:
| Value | Means |
|---|---|
keystroke_stream |
characters arrived as individual input events |
paste |
a clipboard or drop insertion occurred |
import |
content was loaded from a file or external document |
programmatic |
the buffer was modified by the tool or an extension, not the user |
unknown |
the tool genuinely cannot tell — a valid and honest answer |
unknown is not a failure. A tool that cannot distinguish should say so rather than guess.
Mixed activity is reported as multiple observe calls, never averaged into one. Averaging a
paste and a typing burst into a single blended record destroys precisely the distinction the log
exists to preserve. The agent commits all observations in a window to the leaf as a Merkle tree
over the sequence, so a leaf carries each one intact and a holder can later disclose one without
the others. See wire-format.md §3.
POST /v1/commit
Request that accumulated observations become a revision leaf.
// request
{ "session": "s_01J…",
"content": "…", // full buffer; committed as SHA256(0x03‖bytes)
"reason": "idle" } // | save | close | explicit
// response — committed
{ "committed": true,
"seq": 42,
"head": "sha256:…",
"leaf": "sha256:…" }
// response — coalesced (not an error)
{ "committed": false,
"reason": "coalesced",
"retry_after_ms": 1400 }
commit is a request, not a command. The agent may coalesce, defer, or refuse it. An editor
that treats a non-commit as an error is misreading the contract — §4 covers why.
GET /v1/entity/{id}/proof?seq=N
Returns (leaf, inclusion_proof, witness_receipt) — the input to the four-step verifier. If the
enclosing head is not yet witnessed, witness_receipt is null and witness_state is
pending. This is normal and expected; see §5.
4. Coalescing: why commits are advisory
The editor asks for a commit whenever it thinks one is warranted. The agent decides.
This exists because leaf granularity is an evidential property, not a UI one. Too fine and the log is enormous while each leaf proves almost nothing; too coarse and the history says little about how the work was made. That judgement has to be consistent across every tool touching an entity, so it belongs on one side of the boundary — and it is baked into every historic leaf, so it cannot be renegotiated later.
Default policy: coalesce until 90 s idle, or reason: "save" | "close" | "explicit", subject
to the §5 minimum interval. Editors should call commit liberally and let the agent absorb it.
explicit always produces a leaf. It is the one reason the agent may not coalesce away.
explicit means the creator named this moment — “Draft 2 complete”, the version going to a
committee, the state submitted to a publisher. That is the most evidentially significant leaf in
a chain, and it is the only one whose position the creator chose deliberately rather than as a
side effect of when they stopped typing. Absorbing it into surrounding idle work would discard
precisely the boundary that was worth recording.
The §5 minimum interval still applies, so explicit cannot be used to bypass rate limiting — a
client sending it in a loop is refused like any other. It bypasses coalescing, not the floor.
Note that an explicit leaf carries no name. The milestone is the leaf’s position in the chain,
witnessed like any other; a creator-supplied label would have to enter the hashed layer, where it
would become part of what a disclosure reveals. A title can be sensitive on its own. If labelling
is ever wanted it is a format decision, not something a client may add.
duration_ms is measured across the coalesced window, so it stays meaningful regardless of how
often the editor asked.
5. Rate limits
Two tiers, because the two rates have completely different costs.
Leaves — local, generous
| Limit | Default | Behaviour on breach |
|---|---|---|
| Minimum commit interval | 2 s | coalesce, return retry_after_ms |
| Daily leaf budget per entity | 2 000 | 429, Retry-After |
| Observations per minute per session | 600 | drop excess, set "throttled": true |
A leaf costs local disk and one level of Merkle depth. At 2 000 leaves/day an inclusion proof is ~11 hashes; at 100× that it is ~18. Proof size is not the reason for these limits — they exist to catch a runaway client before it fills a disk, not to ration a scarce resource. Tune them up freely.
Witnesses — shared, strict
This is the tier that matters. OpenTimestamps aggregators are a public good operated at someone else’s expense, and every submission consumes calendar capacity that other people are relying on.
| Limit | Default | Rationale |
|---|---|---|
| Witness submissions | 1 per 10 min, process-wide | not per entity — see batching |
| Entities per submission | unlimited | batching is the point |
| Minimum interval per entity head | 1 h | a head is only worth witnessing once it has moved meaningfully |
| Burst allowance | none | there is no legitimate reason to burst a timestamp |
Batching is mandatory, not an optimisation. All pending entity heads are combined into one Merkle root and submitted as a single OTS request. Each entity then keeps an inclusion proof from its head into that batch root, so:
N documents → 1 OTS submission → N inclusion proofs
External load is O(1) per interval regardless of how many documents are open. A client that bypassed batching and submitted per-entity would be the exact bad-citizen behaviour these limits exist to prevent — which is why the editor cannot reach the witness at all. It has no path to it; only the agent does.
Backpressure
The agent never drops work silently. A refused commit is queued or explicitly reported with
retry_after_ms. A witness submission that is rate-limited stays pending and is picked up in the
next window. Pending is a normal steady state, not an error: OTS proofs begin pending and upgrade
on Bitcoin confirmation ~1 h later anyway, which is why the design specifies a publish-only
cadence in the first place.
Editors must not implement their own retry loops on 429. Honour Retry-After.
6. What a history does not mean
Normative, and aimed at whatever renders this data — the editor, a viewer, a future dashboard.
A DAON history is evidence a creator may choose to offer. It is not a credential, and its absence is not a deficiency. Almost all writing that exists was made without one, including almost all of the writing anyone reading a history has ever valued.
Absence and sparseness carry no meaning
- No history is the norm. Anyone writing in Google Docs, on paper, in Notes, or in any tool that has never heard of this spec has none. That is the overwhelming majority of all work.
- A thin history is not a weak one. Someone who saves twice a day produces a fraction of the leaves of someone who saves constantly. The difference is a habit, not a fact about authorship.
- A creator who never discloses has said nothing, and must not be treated as having said
something. Default silence is the design’s position (
provenance-data-model.md), not evasion.
The metrics describe writing, not authorship
op_count, duration_ms, span_bytes and ingress record mechanism. They do not measure
effort, originality, or honesty, and they are not comparable between people:
| Pattern that looks “suspicious” | What it actually is |
|---|---|
Mostly paste |
drafted somewhere else — a notes app, Docs, a phone, a collaborator’s message |
| Very few, very large leaves | writes in long sittings; saves rarely |
Fast op_count, short duration_ms |
a touch typist, or dictation, or an assistive input device |
| Long gaps, sporadic bursts | a job, children, illness, a life |
Every row is ordinary. A reading that treats any of them as a red flag is not detecting anything — it is penalising people for how they work, and it will land hardest on the people with the least control over how they work.
Therefore, implementations must not
- Score, rank, grade, or rate a history, or compute any single number intended to summarise its trustworthiness.
- Compare one creator’s history to another’s, to an average, or to a threshold.
- Present absence as negative — no “unverified” badges, no warning colours, no empty-state copy implying something is missing or wrong.
- Present
pasteas negative — no highlighting, no distinct warning styling, no ordering that surfaces it as exceptional. - Nudge toward more leaves — no streaks, no completeness meters, no prompts to save more often “for better provenance.” That converts a record into a performance.
A tool that ships any of these has built the gatekeeping instrument this project exists to refuse, regardless of what its documentation says.
Why this is in a wire format spec’s companion
Because the pressure is structural, not accidental. The moment a history is legible, someone will want to read it as a verdict — and the people asking will usually have more power than the person being asked. The format’s job is to leave that reading unsupported: no score to cite, no comparison to draw. Implementations must not reintroduce at the presentation layer what the format deliberately withholds.
The distinction to hold onto is capability versus surface. A creator may choose to prove a
single segment of a revision (wire-format.md §6) — that exists so they can disclose less,
since the alternative is revealing an entire manuscript to answer one question. But nothing this
system issues, renders, or accepts works at that grain. There is no endpoint taking a segment
index, no certificate showing one, no view that invites the question. What a holder can
volunteer and what an adversary can demand are not the same surface, and only the second one is
ours to close.
7. What the editor must never do
- Infer or report content source. No
source,is_human,ai_generated, or equivalent — including viatool_id,ingress, or a vendor extension field. - Reach the witness directly. All egress is the agent’s.
- Send timestamps as truth.
local_timeis recorded as creator-asserted and untrusted; the beacon and witness establish real bounds. - Retry around a limit. Honour
Retry-After. - Fabricate
ingress.unknownis always available and always acceptable. - Emit content outside
commit. Observations carry counts and spans, never bytes.
8. Conformance checklist
- Reads
limitsfromsession/openrather than hard-coding §5 - Treats
committed: falseas normal flow, not an error - Reports
ingress: "unknown"rather than guessing - Emits separate observations for mixed activity instead of averaging
- Honours
Retry-Afterwith no client-side retry loop - Contains no field, anywhere, asserting content source
- Never opens a network connection on the versioning layer’s behalf
- Connects over a Unix socket or named pipe, never TCP — including in debug builds
- Displays no score, rank, grade, or trustworthiness summary of a history
- Compares no history to another, to an average, or to a threshold
- Styles absence and
pasteneutrally — no badges, warnings, or highlighting - Offers no streak, meter, or prompt encouraging more frequent commits
- Opens one session per assembled artifact, not per file (§3)
- Treats identical content under different entities as normal, never as duplication
- Sends
reason: "explicit"only for creator-named milestones, and never in a loop
9. Open questions
- Content store.
content_commitis over content bytes, not a delta (wire-format.md§6), so storage is now purely a local decision with no effect on any commitment. Git remains a strong candidate — a content-addressed delta store with append-only history — and choosing it no longer constrains anything an adjudicator has to reproduce. - Key loss. Losing the author key ends the ability to append to an entity; history stays verifiable but frozen. Needs a recovery story before anyone depends on this.
- Multi-device. Out of MVP scope (single-writer), but the
seq/parent_headshape should be checked against a future where two devices append to one entity. - Limit defaults. The §5 numbers are reasoned, not measured. Revisit against real editing traffic once instrumented.