Where it fits
Receipts record what happened. Envelopes record what was authorized. Neither records a claim that either one reflects harm — that claim, when it exists, has historically lived unsigned, in a support ticket or a chat log. An abuse report is the accusation complement to both: it doesn't replace a receipt or an envelope, it points at one, by hash, as the basis for a claim that it reflects harm.
Receipts say what an agent did. Envelopes say what an agent may do. A report says someone claims one of those was harmful, and grounds that claim in the other two. Together they give a system four tenses of accountable evidence — capability, decision, history, and accusation — each independently verifiable, none requiring trust in the party that produced it, and none silently overreaching into what it cannot prove.
The thirteen fields
A report is a JSON object with exactly these thirteen members. A document with any additional
member, any missing member, an accused_role, category, or
delegation_position outside its closed set, or a malformed evidence_refs
is not a report and MUST fail verification.
| Field | Type | Requirement |
|---|---|---|
| protocol | string, "abuse-report/0.3" | REQUIRED |
| reporter_id | string, non-empty | REQUIRED |
| accused_id | string, non-empty | REQUIRED |
| accused_role | string, one of agent | principal | REQUIRED |
| category | string, closed vocabulary or other | REQUIRED |
| evidence_refs | array, non-empty, of {type, hash} | REQUIRED |
| narrative | string, non-empty | REQUIRED |
| delegation_position | string, one of upstream | downstream | lateral, or null | REQUIRED; null when no topology claim is asserted |
| delegation_ref | string (64-char lowercase hex SHA-256) or null | REQUIRED; null when the filer relied on standing authority |
| prev_hash | string (64-char lowercase hex SHA-256) or null | REQUIRED; null only for a reporter's first report |
| issued_at | string, RFC 3339 timestamp | REQUIRED; supplied by the issuer, validated by parsing |
| sig | string (128-char hex Ed25519 signature) | REQUIRED |
| pubkey | string (64-char hex, raw 32-byte Ed25519 public key) | REQUIRED |
delegation_position and delegation_ref are always present, never
omitted — the same convention prev_hash already uses. A well-formed report
has thirteen members whether or not it makes a delegation claim; the fields are just null
when it doesn't.
The category vocabulary
A closed, versioned set: unauthorized_scope_expansion, deceptive_output,
resource_abuse, collusive_behavior — an agent accused of abusing a
principal — and injection_attempt, capability_extraction,
harassment — a principal accused of abusing an agent — plus other, where
narrative carries the substantive claim. Unchanged in abuse-report/0.3.
A new category is a lightweight, maintainer-approved protocol revision — real narratives that used
other, argued as behavioral not sectoral — never added silently to an unchanged
protocol value. See Docs & Spec for the full governance
process.
Evidence references
Each element of evidence_refs is exactly {type, hash} — a pointer, never
a bundled payload. type is receipt or envelope;
hash is computed the same way the cited artifact's own reference implementation
computes it. A report may cite more than one artifact — real abuse is often a pattern across
several receipts, not one isolated event — but the reporter MUST be a party to every
cited item, not just one; otherwise an unrelated citation could be smuggled in alongside a
legitimate one.
Delegation topology
accused_role answers one question — is the accused a computational agent or a human
principal? delegation_position answers a different one: where does the accused sit
relative to the filer in the delegation chain — upstream (the accused
authorized or supervises the filer), downstream (the filer delegated to the accused),
or lateral (a peer relationship, no delegation link at all)? The two fields are
orthogonal, not redundant: "upstream" alone doesn't say whether you're dealing with a human at the
chain's root or a supervising agent one hop up, and accused_role alone can't express a
peer-to-peer accusation with no delegation relationship — lateral is expressiveness
the old binary genuinely couldn't reach.
The filer of a report is itself almost always an agent acting under delegation, not a human typing
directly. delegation_ref — a hash pointer to the DAT that authorized the filer, or
null for standing authority, the exact semantics sm-arp's own
authority_chain already uses — is what makes that inheritance checkable: a verifier
holding the DAT can trace the filer's signing key back to the human who authorized it.
Self-asserted, not verified. Neither field is independently checked against
real chain data in this version — sm-arp's own authority_chain is
itself descriptive-only as of its v0.1, so verifying delegation topology here first would verify
against a foundation that doesn't exist yet. A real verify_delegation() is a natural
future addition once that foundation lands.
Canonicalization & signature
The signing input is the JSON serialization of the report with the sig member
removed, using: keys sorted lexicographically at every level; the compact separators
"," and ":" (no insignificant whitespace); UTF-8 encoding.
sig is the Ed25519 signature (RFC 8032) over those canonical bytes, hex-encoded;
pubkey is the raw 32-byte Ed25519 public key, hex-encoded. Identical inputs produce
byte-identical reports, and hostile input never raises out of the verifiers.
report {
protocol "abuse-report/0.3"
reporter_id the filer's identity
accused_id the accused party's identity
accused_role agent | principal
category closed vocabulary, or "other"
evidence_refs [ { type: receipt|envelope, hash } , ... ] (non-empty)
narrative the filer's account (required)
delegation_position upstream | downstream | lateral | null
delegation_ref SHA-256 of the DAT behind the filer, or null
prev_hash SHA-256 of this reporter's prior report, or null
issued_at RFC 3339, caller-supplied
sig Ed25519 over the canonical body, sig removed
pubkey the reporter's raw Ed25519 public key
}
Report hash and the chain
The report hash is the lowercase-hex SHA-256 of the full report — including sig —
under the same key ordering and separators. A successor report's prev_hash must equal
the report hash of the same reporter's immediately preceding report. A reporter's chain is
reconstructed the same way an agent's envelope chain is: rejecting a gap (a deleted interior
report), a fork (two reports claiming the same predecessor), a foreign-chain splice (a
prev_hash reaching into another reporter's chain), and duplicates or zero/multiple
genesis reports. Since abuse-report/0.3, this chain isn't reports-only — see
Retraction, below.
Retraction
A retraction is a separate, eight-field object — not a report variant — a reporter withdrawing
one of their own prior reports: protocol, reporter_id,
retracted_report_hash (the report being withdrawn, by hash), reason
(required, non-empty), prev_hash, issued_at, sig,
pubkey. It carries no accused_id, category, or
evidence_refs — none of those apply to a statement about a prior accusation rather
than an accusation itself.
Reports and retractions from one reporter share one hash chain — a reporter can't
selectively hide a retraction any more than they could hide a report. order_chain
accepts either kind, mixed, type-dispatched by field-set shape. A new resolver,
retraction_status, walks an already-ordered chain and returns the report hashes
currently retracted: a report is retracted if a later valid retraction in that same chain names
it.
Retraction doesn't erase. Retracting a report never touches its own
signature — verify_report on the original still returns true
forever. You cannot un-sign something, only say, also signed, that you withdraw it. There is
no un-retraction (once issued, permanent) and no partial retraction of a single
evidence_refs entry — retracted_report_hash always names one whole
report.
Two-tier verification
A report's signature and structure can always be checked, with no external lookups. Whether its citation is real — whether the hash resolves to something, and whether the reporter was really a party to it — can only be checked by someone who holds the cited artifact. Conflating the two would mean either rejecting reports nobody happens to be able to fully audit yet, or silently trusting citations that were never actually confirmed. Both are wrong, so the check is split:
Always available
Format, canonical signature, and chain integrity across whatever set of a reporter's reports the verifier holds. No access to the cited evidence required.
Requires holding the artifact
Recomputes the artifact's hash, checks its declared type, and confirms the reporter is a real party to it. Returns confirmed, hash_mismatch, type_mismatch, or not_party.
A citation nobody has checked this way at all — the verifier simply doesn't hold the artifact — is unconfirmed, a distinct, honest state. Treating "not checked" as either "passed" or "failed" is a misuse of the primitive, not a property it provides.
What it does not do
Signs the accusation, not the verdict
A report is an accusation, not a finding. No report, however well-formed and evidence-confirmed, determines guilt or innocence.
No automatic score consequence
A confirmed report does not by itself change any reputation score. A future integration is a named direction, not built here.
Deployment policy, not this primitive
Who collects, stores, or distributes reports is outside this specification, the same boundary drawn around who runs a reputation auditor.
Out of scope. Whether an action actually happened, and whether it was authorized, are both outside this primitive — those are agency receipts' and agent envelope's questions. An abuse report only ever claims: someone accused this, grounded in that.