Witness-DAO — Data Protection & Regulatory Compliance Audit
Auditor role: data protection / regulatory compliance due diligence (not legal advice; findings are framed as risks and questions for counsel).
Subject: C:\Users\miker\cmd-agent\witness-DAO\witness-dao — @hypersolid/witness-dao v0.1.0
Date: 2026-07-25
Method: read-only review of README.md, LIMITATIONS.md, governance-rules/, and all 41 source files under src/. No file was modified. Line references are to the tree as read; parallel edits may shift them by a few lines.
Standing caveat: I am not a lawyer and neither is the reader. Everything below is a risk assessment grounded in what the code does. Every conclusion marked (analysis) is a question for qualified counsel, not a determination.
Verdict
No — not in its current state, and the blocker is documentation and architecture rather than code quality. The engineering here is unusually honest and unusually careful; the codebase's evidentiary discipline is better than most commercial products I have reviewed. But the product is, in substance, a system for permanently attributing behavioural records to identified natural persons — wallet address, voting choice, voting power, free-text rationale, treasury destination — and then anchoring digests of those records to public, irreversible, non-EU infrastructure. Across 21,982 lines of source, four markdown rule files, a README and a LIMITATIONS file, the strings "GDPR", "personal data", "data subject", "retention", "erasure" and "data protection" appear exactly zero times. There is no deletion path, no retention policy, no residency control, no controller/processor model, no sub-processor register, no minimisation of free-text ingestion, and no rectification mechanism — in a product whose entire architecture is designed to make all of those impossible. An EU-facing enterprise could not complete a DPIA on this today, and its DPO would have nothing to rely on. The good news is that the hard part — the erasure collision — is more tractable than it first appears (only digests leave the building; see the dedicated section), so the gap is bridgeable with roughly a quarter of documentation, contractual and configuration work rather than a rewrite. The over-claim risk is smaller than feared but real in three specific places: EU AI Act Article 12, MiCA/CASP, and eIDAS.
Findings table
| ID | Title | Severity | Basis | Code reference | Impact (one line) |
|---|---|---|---|---|---|
| C-01 | No data-protection design, vocabulary or documentation anywhere in the product | Critical | Confirmed | absence across src/, README.md, LIMITATIONS.md |
An EU enterprise cannot complete a DPIA or an Art. 30 record from what ships. |
| C-02 | No erasure, rectification, retention or crypto-shredding capability exists; the architecture is affirmatively anti-erasure | Critical | Confirmed | src/seal/witness-client.ts:103-416 (no delete method); src/seal/sealer.ts:334-348, :359-365 |
Art. 17 / Art. 16 / Art. 5(1)(e) cannot be satisfied by any mechanism the product offers. |
| C-03 | Sealed records are directly-attributable behavioural profiles of identified natural persons | High | Confirmed | src/seal/sealer.ts:167-189; src/binding/snapshot.ts:332-352; src/binding/governor.ts:326-353; src/binding/safe.ts:253-278 |
This is personal-data processing in substance whatever the pseudonymity framing says. |
| C-04 | Unbounded free-text ingestion into permanent records; no PII filter, no operator opt-out | High | Confirmed | src/binding/snapshot.ts:304-306, :346; src/binding/governor.ts:284-286, :345-346; src/binding/safe.ts:324-348 |
Arbitrary third-party PII can be sealed and anchored irreversibly, with no gate. |
| C-05 | Art. 9 special-category inference risk from governance content is unacknowledged | High | Analysis | src/binding/snapshot.ts:343-346; src/binding/governor.ts:339-346 |
Voting patterns plus free-text rationale on political/policy proposals plausibly reveal political opinions. |
| C-06 | No controller/processor model; anchor targets are undocumented de facto sub-processors | High | Confirmed | src/anchor/ots.ts:110-115; src/anchor/rfc3161.ts:96-99; src/anchor/rekor.ts:81 |
Seven third parties are contacted by default with nothing a DPO can point at. |
| C-07 | No data residency or region pinning in the configuration surface | High | Confirmed | src/config.ts:26-36, :81-111 |
Chapter V transfer analysis cannot be constrained or evidenced; no EU-only mode. |
| C-08 | Art. 14 notice obligation to third-party data subjects is structurally unmet | High | Analysis | src/binding/snapshot.ts:250-271; src/binding/safe.ts:457-475 |
Data is harvested about voters/signers who are not the customer and are never told. |
| C-09 | EU AI Act Article 12 framing is a category error for this product | High | Confirmed | src/seal/witness-client.ts:345-356 |
Invoking AI-system record-keeping for human governance actions would embarrass the vendor before a regulator. |
| C-10 | AML/sanctions screening is mandated by shipped rule text but unimplemented; if implemented as written it creates Art. 10-adjacent exposure | High | Confirmed | governance-rules/treasury.transfer.md:10-12; no implementation in src/ |
A permanently anchored sanctions allegation about a named person is close to irreparable. |
| C-11 | Local state directory accumulates plaintext personal data indefinitely with no purge | Medium | Confirmed | src/cli/state.ts:41-45, :98-156 |
Storage-limitation failure on the customer's own disk, invisible to their retention tooling. |
| C-12 | No rectification / supersession mechanism, and reports do not surface superseded records | Medium | Confirmed | src/seal/sealer.ts:334-348, :359-365; nothing in src/report/ |
A wrong record stays in every future report with no marker; Art. 5(1)(d) and evidentiary risk. |
| C-13 | eIDAS framing implies more than non-qualified DigiCert/Sectigo timestamps deliver | Medium | Confirmed | README.md:68; src/anchor/index.ts:9; src/anchor/rfc3161.ts:2-11, :96-99 |
No Art. 41(2) presumption of accuracy is available; a reader may assume there is. |
| C-14 | MiCA / CASP framing overreaches what the tool does | Medium | Confirmed | README.md:17; package.json keywords: [... "mica"] |
Sets a compliance expectation the product does not and cannot meet. |
| C-15 | The strongest absence basis is unreachable with shipped providers, and reaching it requires sealing the complete voter roll | Medium | Confirmed | src/binding/snapshot.ts:441; src/report/liability.ts:349-380 |
Data-maximisation incentive baked into the evidence calculus; headline claim not deliverable out of the box. |
| C-16 | No lawful basis, notice or subject-relationship model for the liability-defence pack | Medium | Analysis | src/report/liability.ts:513-560; src/mcp/server.ts:520-538 |
Anyone with an API key can generate a dossier on any address in the DAO. |
| C-17 | RFC-3161 requests use plaintext HTTP | Low | Confirmed | src/anchor/rfc3161.ts:96-99 |
Anchoring cadence and volume leak to network observers; no confidentiality on the request. |
| C-18 | MCP tool surface exposes a named individual's dossier to an autonomous agent | Low | Confirmed | src/mcp/server.ts:506-538, :759, :794-801 |
Personal data flows into an LLM context with no authz, no purpose limitation, no access log. |
| C-19 | Report determinism, reportDigest and the non-weakenable disclaimer are genuinely strong |
Info | Confirmed | src/report/governance.ts:1252-1256; src/report/disclaimer.ts:39-216 |
Positive: materially improves admissibility and reduces over-claim risk. |
Counts: Critical 2 · High 8 · Medium 6 · Low 3 · Info 1 (19 findings).
Per-finding detail
C-01 — No data-protection design, vocabulary or documentation anywhere in the product · **Critical** · Confirmed
What the code does. Nothing. That is the finding. A full-text sweep of src/, README.md, LIMITATIONS.md and governance-rules/ for gdpr|personal data|pii|privacy|data subject|erasure|right to be forgotten|pseudonym|anonymi|retention|purge|ttl returns only incidental hits: redact() in src/core/http.ts:206-226 (which redacts credentials from URLs, not personal data), and REDACTED in src/cli/exit.ts:198-235 (same purpose). LIMITATIONS.md — a document whose stated premise is "a limitation documented here is worth more than a limitation discovered by opposing counsel" — has a four-item "Product / legal caveats" section covering legal advice, commercial thesis, attestation ownership and the RECORDED verdict, and says nothing about data protection.
Regulatory exposure. This is not a cosmetic gap. An enterprise deploying this is a controller under Art. 4(7) and owes: Art. 30 records of processing, an Art. 35 DPIA (near-certainly required — see below), Art. 13/14 transparency, an Art. 6 lawful-basis determination, Art. 28 processor contracts, and a Chapter V transfer assessment. The vendor supplies none of the inputs. Worse, the DPIA trigger is strong on the EDPB's own criteria: evaluation or scoring of individuals, systematic monitoring, data processed on a large scale, matching/combining datasets (Snapshot + Safe + on-chain + git), and data concerning vulnerable subjects or with a legal/similarly significant effect — the product's explicit selling point is that its output bears on personal legal liability. Three or more criteria is the EDPB's threshold; this hits four or five.
Recommendation. Ship a DATA-PROTECTION.md alongside LIMITATIONS.md, written in the same honest register, covering: the data categories actually sealed (enumerate them from decision.inputs), the recipients (VDA, plus every default anchor target), the retention position, and the erasure position. Then produce a DPIA template and a controller/processor allocation memo. Question for counsel: does the vendor's own generation of the liability pack (as opposed to the customer's) make the vendor a joint controller for that specific purpose?
C-02 — No erasure, rectification, retention or crypto-shredding capability · **Critical** · Confirmed
See The erasure analysis below — this is the headline finding and gets its own section.
Short form: WitnessClient (src/seal/witness-client.ts:103-416) exposes seal, verifyRemote, getProofBundle, getChainProof, listRecords, listAllRecords, getRecord, usage, report, getDidDocument, mintTestKey. There is no deleteRecord, no redactRecord, no expireChain, no retention parameter on any call. src/cli/state.ts has no purge. The only acknowledgement of the problem in the whole codebase is a code comment at src/seal/sealer.ts:363-365: "catching it later costs an unreportable record that is already anchored and cannot be corrected without appending a second record."
C-03 — Sealed records are directly-attributable behavioural profiles of identified natural persons · **High** · Confirmed
What the code does. Sealer.buildRequest writes seven fields into decision.inputs, which is the canonical body that bodyHash digests, that the Ed25519 signature covers, and that the Merkle root commits to (src/seal/sealer.ts:167-189). The comment at :172-177 is explicit: "These seven fields ARE the evidence." They are:
kind,eventId,daoId,occurredAtactors— "Addresses that acted (voters, confirming signers, executor). Lowercased." (src/core/types.ts:87-88)binding— includingtxHash,proposalId,ipfsCid,safeTxHash,signatureDigestdetail— an unconstrainedRecord<string, unknown>populated by the providers
Additionally decision.reasoning (src/seal/sealer.ts:310-332) is a free-text sentence that interpolates the full actor list: `Actors: ${event.actors.join(', ')}`. So every sealed record carries the addresses twice.
What the shipped providers actually put in detail:
| Source | Personal / attributable content sealed | Reference |
|---|---|---|
Snapshot vote.cast |
voter address (in actors), choice, choiceLabels, votingPower, free-text reason (2,000 chars), proposalId |
src/binding/snapshot.ts:332-352 |
Snapshot proposal.created |
author address, title, bodyExcerpt (4,000 chars), bodyDigest, choices, public url |
src/binding/snapshot.ts:291-319 |
Governor vote.cast |
voter, support, supportLabel, weight, free-text reason (2,000 chars), reasonDigest, params |
src/binding/governor.ts:326-353 |
Governor proposal.created |
proposer, targets, values, calldatas (selector, digest, 138-char preview), descriptionExcerpt (4,000 chars) |
src/binding/governor.ts:260-292 |
Safe multisig.confirmation |
signer owner address, signatureType, confirmationsRequired, confirmationsCollected |
src/binding/safe.ts:253-278 |
Safe multisig.execution |
executor, proposer, confirmingOwners (the full sorted signer list), to, value, selector, decodedMethod |
src/binding/safe.ts:322-349 |
Safe treasury.transfer |
ERC-20 to (recipient address), from, amount, executionTxHash |
src/binding/safe.ts:368-390, :623-653 |
Is a wallet address personal data? (Analysis.) The orthodox GDPR answer is that a pseudonymous identifier is personal data where the controller or any other party can reasonably link it to a natural person (Recital 26; Breyer C-582/14; and the CJEU's 2025 EDPS v SRB judgment, which turned on whether the recipient had reasonable means of re-identification). DAO culture asserts that addresses are pseudonymous and therefore out of scope. In this product that argument collapses on its own value proposition. The liability-defence pack takes a subject address (src/report/liability.ts:543, src/mcp/server.ts:794-801), is explicitly built "for a named individual" (src/report/liability.ts:1-2), is targeted at "the wrapper, the signers, and their counsel" (README.md:23), and exists so that a named human can hand it to a court. The customer therefore by design holds the linkage between address and identity — that linkage is the product. A vendor cannot simultaneously sell "we attribute governance actions to your named signer" and argue "these are just anonymous hex strings". Combined with voting power (a proxy for holdings), timestamps, and free-text rationale, an in-scope record set is a behavioural profile.
Recommendation. Stop treating this as an open question internally and state the position: addresses processed by Witness-DAO are personal data, and the product is designed for a purpose that makes them so. Document the categories in the table above verbatim so a DPO can copy them into an Art. 30 record. Question for counsel: confirm the position, and confirm whether the Safe confirmingOwners array (a named group's collective decision) attracts any additional treatment.
C-04 — Unbounded free-text ingestion into permanent records · **High** · Confirmed
What the code does. Four free-text paths reach the signed body, all truncated for size but never filtered for content:
src/binding/snapshot.ts:304—bodyExcerpt: p.body.slice(0, 4_000)(withbodyDigestat:305committing to the full text)src/binding/snapshot.ts:346—reason: v.reason.slice(0, 2_000)src/binding/governor.ts:284—descriptionExcerpt: decoded.description.slice(0, 4_000)src/binding/governor.ts:345—reason: reason.slice(0, 2_000)
Plus src/binding/governor.ts:281 — preview: cd.slice(0, 138) of raw calldata, and the free-form governing ruleText (src/binding/rules.ts), which is operator-authored markdown sealed verbatim into every record.
The truncation comments are candid about why they truncate: "Full body can be enormous; keep a bounded excerpt plus a digest" (snapshot.ts:302-303). Size, not privacy. Sealer validates that detail is a plain object (src/seal/sealer.ts:408-414) and nothing else. There is no allow-list, no redaction hook, no detail schema, and no way for an operator to say "seal the digest of the body but not the text".
Regulatory exposure. A DAO proposal body routinely contains names, emails, grant applicant details, contributor compensation, dispute narratives and sometimes accusations. A vote reason is a natural-language political statement. All of it becomes part of a signed, hash-chained, Merkle-committed record with no deletion path. This is a straightforward Art. 5(1)(c) minimisation failure: the evidentiary purpose — proving that a proposal existed with a given text — is fully served by bodyDigest alone, which the code already computes. The excerpt is a convenience, and it is the convenience that creates the irreversible exposure.
Recommendation. Concrete design change, and the highest-value one in this audit: make excerpt sealing opt-in rather than default. bodyDigest + bodyLength alone preserve tamper-evidence and re-derivability; the plaintext can live in the customer's own erasable store keyed by digest. Add a detailPolicy seam on the providers ({ sealExcerpts: boolean; redactor?: (d) => d }) so an enterprise can inject its own PII scrubber before anything reaches buildRequest. Document that once sealed, text cannot be removed.
C-05 — Art. 9 special-category inference risk unacknowledged · **High** · Analysis
What the code does. Seals, per person: voting choice on each proposal, the labelled meaning of that choice (choiceLabels, snapshot.ts:344; supportLabel, governor.ts:340), voting power, and a free-text rationale.
Regulatory exposure. (Analysis.) Art. 9(1) covers data "revealing" political opinions. The CJEU's expansive reading in OT v Vyriausioji tarnybinės etikos komisija (C-184/20) held that data from which special-category information can be inferred by intellectual operation falls in scope. DAO governance is not party politics, but many DAOs vote on grants to political causes, on jurisdictional positioning, on sanctions/censorship policy, on treasury exposure to particular states. A longitudinal record of one identified person's votes plus their written reasoning is exactly the sort of dataset from which political opinion is inferable — and, unlike an ordinary voting record, it is permanent and externally anchored. Trade-union membership and philosophical belief are also conceivable in DAOs organised around labour or ideological aims.
Note the asymmetry that makes this worse than the usual inference argument: if a supervisory authority later concludes that a given DAO's sealed record set falls under Art. 9, there is no remediation available — the records cannot be deleted (C-02) and the Art. 9(2) derogations are narrow (explicit consent, or Art. 9(2)(f) legal claims, which is at least arguable here).
Recommendation. Add an explicit risk statement to the DPIA template: "governance content may reveal Art. 9 categories depending on the DAO's subject matter; this must be assessed per-deployment before the first seal, because it cannot be assessed retrospectively." Reinforces the C-04 recommendation — sealing digests rather than text substantially defuses this. Question for counsel: where a DAO's proposals are manifestly political, does Art. 9(2)(e) ("manifestly made public by the data subject" — Snapshot votes are public) provide adequate cover, or is that too thin given the aggregation and permanence the product adds?
C-06 — No controller/processor model; anchor targets are undocumented de facto sub-processors · **High** · Confirmed
What the code does. By default, a single seal + anchor cycle contacts:
| Party | Endpoint | Set at | What it receives |
|---|---|---|---|
| VDA Witness (SaaS) | https://witness.getvda.ai |
src/seal/witness-client.ts:32 |
The full plaintext decision body, including actors, detail, reasoning, governingRule (:139-144) |
| OpenTimestamps calendar A | https://a.pool.opentimestamps.org |
src/anchor/ots.ts:111 |
32-byte Merkle root only |
| OpenTimestamps calendar B | https://b.pool.opentimestamps.org |
src/anchor/ots.ts:112 |
32-byte Merkle root only |
| Eternity Wall | https://a.pool.eternitywall.com |
src/anchor/ots.ts:113 |
32-byte Merkle root only |
| Catallaxy | https://ots.btc.catallaxy.com |
src/anchor/ots.ts:114 |
32-byte Merkle root only |
| DigiCert TSA (US) | http://timestamp.digicert.com |
src/anchor/rfc3161.ts:97 |
32-byte digest in a MessageImprint |
| Sectigo TSA (US) | http://timestamp.sectigo.com |
src/anchor/rfc3161.ts:98 |
32-byte digest in a MessageImprint |
| Sigstore Rekor (Linux Foundation, US) | https://rekor.sigstore.dev |
src/anchor/rekor.ts:81 |
sha256(root), an ECDSA signature, and the operator's public key (:226-244) — published permanently and publicly |
| Snapshot Hub, Safe Tx Service, customer RPC | configurable | src/config.ts:33-35 |
read-side only; but the customer's query pattern reveals which addresses they are investigating |
The README describes anchoring as "a choice" and enumerates the trust properties beautifully (README.md:66-73), but nowhere is any of this framed as a recipient of personal data or a processor/sub-processor. There is no vendor-supplied list an enterprise's DPO can attach to an Art. 30 record, and no notification mechanism if the default calendar list changes in a future release.
Regulatory exposure. The role allocation (analysis) most likely runs: the enterprise/legal wrapper is controller (it determines purpose — defending its signers); VDA is a processor under Art. 28 and needs a DPA with the full Art. 28(3) content, audit rights and a sub-processor clause; the DAO itself is awkward — an unincorporated association or general partnership (which is precisely the Ooki/Lido point) may be a joint controller with the wrapper, and if the DAO has no legal personality the liability lands on the wrapper anyway. The anchoring authorities are the interesting case: they receive only digests, so whether they are processors at all depends on whether a digest of personal data is personal data in their hands (see the erasure section). The defensible position is that they are recipients of pseudonymised data rather than processors, but that position must be written down and reasoned, not assumed.
Recommendation. (a) Publish a sub-processor register with the table above, plus a commitment to notify on change. (b) Make the anchor target list an explicit, logged configuration decision rather than a silent default — src/config.ts already supports WITNESS_DAO_OTS_CALENDARS and WITNESS_DAO_REKOR_URL, but there is no equivalent for the TSAs (opts.tsas exists at src/anchor/rfc3161.ts:1340 but has no env binding). (c) Produce a role-allocation memo. Question for counsel: is a DAO-as-general-partnership capable of being a joint controller, and if so who signs the Art. 26 arrangement?
C-07 — No data residency or region pinning · **High** · Confirmed
What the code does. The full configuration surface is src/config.ts:26-36: WITNESS_API_KEY, WITNESS_BASE_URL, WITNESS_DAO_RULES_DIR, WITNESS_DAO_ANCHORING, WITNESS_DAO_REKOR_URL, WITNESS_DAO_OTS_CALENDARS, WITNESS_DAO_SNAPSHOT_URL, WITNESS_DAO_RPC. WitnessDaoConfig (src/core/types.ts:339-360) has no region, residency, jurisdiction or data-location field. witness.baseUrl is validated only as a syntactically valid http(s) URL (src/config.ts:172-191). There is no "EU-only" mode, no way to assert that sealing must not leave the EEA, and no way to fail closed if it would.
Regulatory exposure. Chapter V. The plaintext body goes to VDA at an unspecified processing location. DigiCert and Sectigo are US entities; Rekor is operated by the Linux Foundation (US) and its content is public by design. Two of the four OTS calendars are operated by parties whose jurisdiction is not documented in the code. For the plaintext leg (VDA) this is unambiguously a transfer of personal data requiring an Art. 46 mechanism — SCCs plus a transfer impact assessment addressing US government access, or reliance on the EU-US Data Privacy Framework if VDA is certified. For the digest legs, see the erasure section — the transfer analysis is much more favourable, but "much more favourable" is not "documented".
Recommendation. Add a residency block to WitnessDaoConfig with an allowedRegions list and a strict flag that refuses to seal or anchor to a target outside it. Even a purely declarative version (config asserts the expectation; the client validates the resolved base URL against an allow-list) is worth having, because it converts an assumption into a checkable control. Ascertain and publish VDA's processing location. Determine DPF certification status for VDA, DigiCert and Sectigo.
C-08 — Art. 14 notice obligation to third-party data subjects is structurally unmet · **High** · Analysis
What the code does. SnapshotProvider.listEvents enumerates every vote on every in-scope proposal (src/binding/snapshot.ts:250-271) and SafeProvider pages the Safe Transaction Service for every multisig transaction and confirmation (src/binding/safe.ts:457-475). Neither the voter nor the signer is the customer. Neither is asked. Neither is told.
Regulatory exposure. Where personal data is not obtained from the data subject, Art. 14 requires the controller to provide identity, purposes, legal basis, recipients, transfer information, retention period and rights — within a month. The controller here is the enterprise, and it will be processing data about potentially thousands of voters it has no channel to. Art. 14(5)(b) excuses notice where it "proves impossible or would involve a disproportionate effort" — genuinely arguable for anonymous token-holders, and the recital contemplates a public-notice substitute. But (i) it is an exemption that must be assessed and documented, not assumed, and (ii) it is much weaker for the small set of named multisig signers, who are identifiable and reachable and whose data is processed most intensively.
Recommendation. Draft a layered privacy notice the customer can publish in the DAO forum and link from the governance rules directory, plus an Art. 14(5)(b) assessment memo distinguishing the anonymous-voter and named-signer cases. Question for counsel: for the named signers whose personal liability the product is defending, is notice plus a documented Art. 6(1)(f) legitimate-interests assessment sufficient, or does the wrapper need something closer to consent given the permanence?
C-09 — EU AI Act Article 12 framing is a category error · **High** · Confirmed
What the code does. src/seal/witness-client.ts:345: /** Upstream EU AI Act Article 12 evidence report. */ on the report() method, which POSTs to /api/witness/report. This is inherited framing from the VDA platform, where the evidence layer exists to satisfy AI Act logging obligations. To the codebase's credit, this is the only AI Act reference anywhere in the repository — the README, LIMITATIONS and all report output are clean of it.
Regulatory exposure. AI Act Art. 12 imposes automatic event-logging ("logs") obligations on high-risk AI systems over their lifetime, so that the system's functioning is traceable. Witness-DAO seals human governance actions: a person's Snapshot vote, a person's multisig signature, a treasury transfer authorised by people. There is no AI system in the loop whose operation is being logged. Calling the resulting artefact an "Article 12 evidence report" is not merely imprecise; it invites two bad outcomes. First, a customer could believe this discharges an AI Act obligation it does not touch. Second, and worse for the vendor: a regulator or opposing counsel who spots it has a free, cheap shot — "you described your output as satisfying a regulation that does not apply to your product; what else have you characterised loosely?" In a product whose entire commercial thesis is calibrated honesty, that is a disproportionate own-goal.
The one honest nuance: if the DAO's own agents seal their decisions through the MCP server (src/mcp/server.js, offered for exactly that at README.md:135-140), those would be AI-agent decisions and Art. 12 could become relevant to the upstream platform. But that is a different product surface, and the comment does not say so.
Recommendation. Change the comment to describe what the endpoint is — "upstream VDA platform evidence report; the AI Act Article 12 framing it carries upstream does not apply to human governance actions sealed by this library" — or drop the characterisation entirely. Cheap fix; disproportionate benefit. Add an explicit "what this is not" line to LIMITATIONS.md §4.
C-10 — AML/sanctions screening: mandated by shipped rule text, unimplemented, and dangerous if implemented as written · **High** · Confirmed
What the code does. governance-rules/treasury.transfer.md:10-12 ships as a working example and states:
"Where an AML or sanctions screening step is performed prior to a transfer, the result of that screening SHALL be recorded alongside the transfer. The screening result is recorded as evidence; it is not a compliance determination."
Rule text is sealed verbatim into every matching record (src/seal/sealer.ts:167-198, governingRule), so this obligation is asserted in the evidence itself. Nothing in src/ implements it. There is no screening provider, no screening field in the treasury.transfer detail (src/binding/safe.ts:368-390), no vendor integration. A grep for sanction|aml|kyc|travel rule across src/ returns nothing.
Regulatory exposure. Two distinct problems.
If it stays unimplemented: the shipped governance rule text asserts a control that the tool does not perform, and that assertion is sealed into records handed to regulators. A DAO adopting the example rule verbatim has committed, in a tamper-evident record, to a screening step it may not be doing. That is a self-inflicted evidentiary wound.
If it is implemented as written: a sanctions screening result is an allegation about an identified person — at minimum a serious reputational assertion, and where the underlying regime is criminal, plausibly Art. 10 GDPR territory (data relating to criminal convictions and offences, processable only under authority of official authority or authorising law). Sealing a "hit" makes it permanent, signed, and committed to Bitcoin. False positives in sanctions screening are common and are normally resolved by re-screening and correcting the record; here there is no correction mechanism (C-12). A person wrongly flagged would carry an anchored allegation forever, and the vendor would have no remedy to offer. This is the single most severe data-protection scenario the product could produce.
Recommendation. Before any AML feature is built: (a) decide, with counsel, whether screening outcomes may be sealed at all, or only the fact and time that a screening was performed against a named provider and ruleset (the latter serves the evidentiary purpose without sealing the allegation); (b) if outcomes must be sealed, seal a digest and a reference, never the disposition text; (c) require an explicit append-correction record type before shipping. In the interim, amend governance-rules/treasury.transfer.md to note that Witness-DAO does not perform screening and to warn that sealing a screening result is irreversible. Question for counsel: is a sanctions-screening disposition Art. 10 data, and does the answer change between "possible match pending review" and "confirmed match"?
C-11 — Local state directory accumulates plaintext personal data indefinitely · **Medium** · Confirmed
What the code does. FileSealJournal persists to <state-dir>/seal-journal.json (src/cli/state.ts:35-39). Its shape (:41-45):
succeeded: Record<eventId, SealedRecord> // SealedRecord carries `decision` incl. inputs
failed: Record<eventId, { request: SealRequest; error: {...} }> // full decision body
recordSuccess (:120-124) and recordFailure (:126-136) write through on every mutation. Files are written mode: 0o600 via temp+rename (:82-87) — good hygiene. But there is no eviction, no TTL, no purge command, no size bound. successes() (:149-151) is used to build reports, so the design assumes the journal is retained indefinitely. FileAnchorRunStore (:165-193) likewise grows without bound.
Regulatory exposure. Art. 5(1)(e) storage limitation applies to the customer's local copy just as much as to the SaaS. The practical risk is that this store is invisible to the enterprise's normal data-lifecycle tooling: it is a JSON file in a scheduled job's working directory, containing every voter address, choice, voting power and free-text reason the pipeline has ever touched. It is also the one copy of personal data here that the customer can actually delete — which makes its absence from any retention discussion a missed opportunity as much as a risk.
Recommendation. Add witness-dao state purge --before <date> and document the state directory as in-scope for the customer's retention schedule. Consider storing only recordId + bodyHash + minimal metadata in succeeded and re-fetching full records from VDA when building reports — the report layer already reads everything from the signed record, so the local copy of the plaintext body is redundant. Note in LIMITATIONS.md that the state directory contains personal data at rest.
C-12 — No rectification or supersession mechanism; reports do not surface superseded records · **Medium** · Confirmed
What the code does. The design position is stated at src/seal/sealer.ts:334-348: a change in the binding "produces a new, additional record rather than silently overwriting the weaker one. Evidence is append-only." That is correct for the integrity purpose. But there is no typed correction: no supersedes field on GovernanceEvent or WitnessDecision (src/core/types.ts:69-133), no correction event kind in GovernanceEventKind (:69-76), and nothing in src/report/ that detects, links or flags a superseded record. A grep for supersede|amend|rectif|correction across src/ returns only a regex in the disclaimer's anti-weakening list (src/report/disclaimer.ts:121) and the comment at sealer.ts:365.
The practical consequence: if a provider seals a record with the wrong actor — say a Safe API returns a stale executor, or an operator's manual event names the wrong signer — that record stays in the record set forever, is graded on its own merits, and appears in every future governance-evidence and liability-defence report with no indication that it is known to be wrong. deriveDecisionId (sealer.ts:341-348) will happily seal a second, corrected record, but nothing links the two, and the report will show both.
Regulatory exposure. Art. 16 gives the data subject the right to rectification, and Art. 19 requires the controller to communicate rectification to each recipient. Neither is achievable. The recognised mitigation for immutable ledgers is append-correction with propagation — you cannot alter the original, but you can ensure every downstream presentation shows the correction. This product does the append and skips the propagation. There is also a straight evidentiary cost: handing counsel a pack containing a record the operator knows to be inaccurate, unmarked, is worse than the inaccuracy itself.
Recommendation. Add an optional supersedes: { recordId, reason } to the sealed inputs, and have report/governance.ts and report/liability.ts mark superseded lines and exclude them from grading (or grade them and annotate — either is defensible, silence is not). This is a genuinely small change with a large compliance and evidentiary return, and it converts an Art. 16 "impossible" into an Art. 16 "satisfied by annotation", which is a much better conversation to have with a supervisory authority.
C-13 — eIDAS framing implies more than non-qualified TSAs deliver · **Medium** · Confirmed
What the code does. Defaults are http://timestamp.digicert.com and http://timestamp.sectigo.com (src/anchor/rfc3161.ts:96-99). The framing:
README.md:68— "Rekor + RFC-3161 for fast, legally-recognised (eIDAS) coverage"src/anchor/index.ts:9— "rfc3161— fast, legally-recognised (eIDAS) commercial timestamps"src/anchor/rfc3161.ts:2— "the fast, legally-recognised anchor"src/anchor/rfc3161.ts:6-8— "its timestamps are the form of evidence European courts and eIDAS-aligned auditors already recognise"
Assessment. Neither of these public endpoints is a qualified electronic time stamp service under eIDAS. Qualified status requires the TSP to be a QTSP listed on an EU Member State Trusted List for that specific service, subject to conformity assessment. DigiCert's and Sectigo's public timestamp endpoints are publicly-trusted commercial timestamping under CA/Browser Forum requirements — a different regime, aimed at code signing. The consequence is precise and matters: eIDAS Art. 41(2) grants a presumption of accuracy of date/time and integrity of data only to a qualified electronic time stamp. A non-qualified timestamp gets Art. 41(1) — it shall not be denied legal effect or admissibility solely because it is electronic or non-qualified. Admissible, yes. Presumptively accurate, no; the party relying on it bears the evidential burden.
To the code's credit, src/anchor/rfc3161.ts:76-83 is scrupulously honest about what is not verified (chain-to-pinned-root, EKU, revocation, validity at genTime), the Rfc3161ChainValidator seam at :701-713 explicitly says "if you need eIDAS-grade reliance", and LIMITATIONS.md §1 flags that CMS verification is untested against a live TSA. So nobody is lying. But "legally-recognised (eIDAS)" in the README — the document a buyer reads — is the kind of phrase a reader converts into "eIDAS-compliant timestamping", and the codebase's own standard is that a reader's reasonable inference is the vendor's problem.
Recommendation. Change the README and anchor/index.ts phrasing to something like "RFC-3161 timestamps from commercial TSAs — admissible under eIDAS Art. 41(1) but not qualified time stamps, so no Art. 41(2) presumption of accuracy applies; supply a QTSA endpoint and a chainValidator if you need that." Then, if the enterprise market wants it, add a documented QTSA configuration path — the opts.tsas seam at :1340 already supports it and just needs an env binding and documentation. Question for counsel: for a Wyoming/RMI wrapper litigating in a US court, does qualified status matter at all? (Probably not — but it matters to the EU enterprise buyer, and the README is addressed to both.)
C-14 — MiCA / CASP framing overreaches · **Medium** · Confirmed
What the code does. README.md:17 lists as a gap the product fills: "AML/MiCA-facing reporting for the legal wrapper that carries the liability." package.json carries "mica" as a published keyword. Nothing else in the repository mentions MiCA, and no code implements anything MiCA-specific.
Assessment. MiCA obligations attach to issuers of asset-referenced/e-money tokens and to CASPs — entities providing crypto-asset services (custody, exchange, transfer, advice, portfolio management) — with the CASP authorisation deadline in July 2026. A CASP's actual MiCA obligations are authorisation, prudential safeguards, governance arrangements under Art. 34 (including record-keeping of all services/activities/orders/transactions under Art. 68), complaints handling, conflicts of interest, custody segregation, and market-abuse controls. Witness-DAO does none of these. What it could honestly contribute is a tamper-evident evidence layer for the governance-arrangements and record-keeping limbs — a CASP with a DAO-adjacent governance structure could use it to evidence that its governance body's decisions followed its documented rules. That is a real, narrow, defensible claim.
"AML/MiCA-facing reporting", by contrast, reads as though the product produces reports that satisfy a MiCA or AML reporting obligation. It does not. It produces evidence that a human then interprets — which is exactly what LIMITATIONS.md:63-66 and disclaimer clause 1 (src/report/disclaimer.ts:42-46) say. The README contradicts the disclaimer.
Note the AML half is even weaker than the MiCA half, because the one AML-adjacent capability the product gestures at is unimplemented (C-10).
Recommendation. Rewrite README.md:17 to the narrow honest claim: "evidence a legal wrapper can put in front of an auditor or supervisor when asked to demonstrate that its governance decisions followed its ratified rules — not a MiCA or AML report, and not a substitute for either." Remove or qualify the mica package keyword. This is the highest-visibility over-claim in the repository and it sits in the second bullet of the README.
C-15 — Strongest absence basis is unreachable out of the box, and reaching it requires maximal collection · **Medium** · Confirmed
What the code does. complete_tally_sealed is the only non-participation basis that can reach PROVEN (src/report/liability.ts:33-36). assessTally (:349-390) will only rely on a proposal.tally record whose actors array is the complete enumerated set of participants, and explicitly rejects a tally with an empty actor set (:368-373): "an empty actor set cannot establish that anyone was absent." Correct reasoning.
But SnapshotProvider.tallyEvent sets actors: [] — deliberately, with the comment "A tally is not an act by a person; actors stay empty by design" (src/binding/snapshot.ts:440-441). No shipped provider emits a tally with a populated actor set (grep "actors:" src/binding/*.ts returns single-actor or empty arrays only). Therefore no record produced by the shipped Snapshot, Safe or Governor providers can ever reach complete_tally_sealed, and the product's headline capability — PROVEN non-participation — is not deliverable with the code as shipped. The best achievable is enumerated_votes_sealed, capped at STRONG (:467-485).
Two distinct problems.
Product/claims: README.md:143-149 presents complete_tally_sealed → PROVEN as an available tier without noting that no bundled provider produces it. That is not dishonest — the table is describing the calculus, and it is accurate about the calculus — but a buyer reading it will assume the strongest tier is reachable. Worth a footnote.
Data protection, and this is the interesting one: to reach the strongest evidentiary tier a customer must build a collector that seals the complete roll of every address that voted on a proposal into a single permanent record. The evidence calculus therefore contains a structural incentive toward maximal personal-data collection: the more comprehensively you enumerate every participant, the better your absent client's defence. Art. 5(1)(c) minimisation and the product's own strength ladder point in opposite directions. That tension is real, it is inherent to proving a negative, and it should be surfaced rather than discovered by a DPO.
Recommendation. (a) Footnote the README table: complete_tally_sealed requires a collector that enumerates the full participant set; the bundled providers do not. (b) Document the minimisation tension explicitly and offer the mitigation: a tally record could seal a digest of the sorted actor set plus the count, and a Merkle root over the actors, allowing a specific absence to be proven by non-inclusion without sealing the whole roll in plaintext. That is a genuinely elegant fix — it preserves the PROVEN ceiling, strengthens the privacy position, and the codebase already has all the Merkle machinery (src/core/merkle.ts). I would rate this the single best design idea to come out of this audit.
C-16 — No lawful basis, notice or subject-relationship model for the liability-defence pack · **Medium** · Analysis
What the code does. buildLiabilityDefenceReport takes a subject string, normalises it lower-case, and produces a dossier (src/report/liability.ts:513-560). The MCP tool requires only daoId and subject (src/mcp/server.ts:520-538). There is no check that the subject consented, that the subject is the requester, that the requester has any relationship to the subject, or that the purpose is defensive rather than investigative. src/report/render.ts:395 renders the subject address prominently at the top of the artefact.
Regulatory exposure. The product is sold as "prepared for" a named individual — implying the subject is the beneficiary. Nothing enforces that. The same call generates an equally detailed dossier on a rival signer, a departing contributor, or anyone whose address is known: what they voted on, what they didn't, when, with how much weight, with their written reasons. That is a profiling capability. Under Art. 6(1)(f) the customer would need a legitimate-interests assessment; defending one's own signer is a strong LIA, building a dossier on a third party is not. Nothing in the product distinguishes the two, records which was intended, or leaves an audit trail of who generated what about whom.
Recommendation. (a) Require and seal a purpose string on report generation, and render it in the artefact — cheap, and it converts an undocumented profiling capability into a purpose-limited one with a paper trail. (b) Log report generation (subject, requester, timestamp) so a controller can answer "who ran a pack on whom". (c) Add a line to the disclaimer or the report header stating the intended defensive purpose and that use for third-party profiling is outside the product's design. Question for counsel: where the subject is not the requester, does the customer owe the subject an Art. 14 notice for the pack specifically, separate from the ingestion notice in C-08?
C-17 — RFC-3161 requests use plaintext HTTP · **Low** · Confirmed
src/anchor/rfc3161.ts:96-99 defaults to http:// for both TSAs. This is conventional for RFC-3161 (the token is signed, so integrity does not depend on TLS, and some TSAs historically served HTTP only), and the code independently checks the nonce echo and the messageImprint (:70-75), so the cryptographic position is sound. The data-protection angle is narrow: a network observer sees which digests the customer timestamps, when, and how often — anchoring cadence and volume metadata, and a correlatable digest that could later be matched against a published Rekor entry. No personal data is exposed. Flagging for completeness and because the appsec audit will want it. Prefer https:// where the TSA supports it; both DigiCert and Sectigo do.
C-18 — MCP surface exposes a named individual's dossier to an autonomous agent · **Low** · Confirmed
src/mcp/server.ts:506-538 registers liability_defence_report as a JSON-RPC tool over stdio, taking subject. The README offers this "for the DAO's own agents" (README.md:135-140). The tool descriptions and UNIVERSAL_CAVEATS (:165-178) are careful about evidentiary honesty — genuinely well done — but the personal-data dimension is absent: an LLM agent can pull a full behavioural dossier on a named person into its context, from which it may be summarised, transmitted or retained by whatever platform hosts the agent. There is no authorisation model (stdio implies the caller is trusted), no access log, and no purpose limitation. This compounds C-16.
Recommendation. Add a caveat to the tool description noting the output contains personal data about an identified individual and should not be forwarded to third-party model providers without a lawful basis. Consider gating the liability tool behind an explicit opt-in flag distinct from the read-only ingest/verify tools.
C-19 — Positive: determinism, report digest, and a non-weakenable disclaimer · **Info** · Confirmed
Covered in What is genuinely well done below.
The erasure analysis
This is the question that determines whether the product is deployable in the EU, and the answer is better than the architecture's rhetoric suggests. The distinction that decides it is what actually leaves the building, and I verified this line by line.
What is published externally
Only digests. Specifically:
MultiAnchorer.anchorRecords(bodyHashes)(src/anchor/anchorer.ts:375-396) builds a Merkle tree over recordbodyHashvalues and anchors only the root. Per-record inclusion proofs are retained locally (:385-388), not published.- OpenTimestamps receives the raw 32-byte root as an octet-stream body to
POST {calendar}/digest(src/anchor/ots.ts:1178-1215). What eventually lands in a Bitcoin transaction is an aggregated commitment several hash-operations removed from our root. - RFC-3161 puts the root in a
MessageImprint(src/anchor/rfc3161.ts:18-30). The TSA returns a token; nothing of ours is published. - Rekor — the only genuinely public, permanent, indexed publication.
buildHashedRekordEntry(src/anchor/rekor.ts:219-245) publishessha256(root), an ECDSA signature over the root, and the operator's public key. Nothing else. And note Rekor is off by default — it requires both a signing key and a pinned log key, and refuses to reachverifiedwithout them (LIMITATIONS.md§2). Zero-config anchoring is RFC-3161 + Bitcoin.
Nothing containing plaintext, wallet addresses, voting choices, proposal text or amounts reaches Bitcoin, Rekor or any TSA. The only party receiving plaintext personal data is VDA Witness, via POST /api/witness/seal with the full decision and governingRule (src/seal/witness-client.ts:134-148).
That single fact converts the erasure problem from catastrophic to manageable.
Is an anchored digest personal data?
(Analysis — this is the pivotal legal question and needs counsel.)
A bodyHash is a SHA-256 digest over a canonical JSON body containing a wallet address, a vote and a timestamp. Under Art. 4(5) that is textbook pseudonymisation, not anonymisation: the additional information needed to attribute it (the preimage) exists, held by the controller and by VDA. WP29 Opinion 05/2014 is explicit that hashing is pseudonymisation, and that a hash of a value drawn from a small, guessable domain is vulnerable to brute-force re-identification.
Two features make the position here materially stronger than the generic "hashes are personal data" worry:
- What is anchored is a Merkle root, not a per-record hash. The root is a hash of hashes over an entire time window's records. Brute-forcing a root requires guessing the complete leaf set and their order — computationally hopeless, and structurally different from hashing a single email address.
- The preimage domain is not guessable. A
bodyHashpreimage is a canonical JSON body containingrecordId,seq,sealedAt,chainKey, the fulldecisionincluding a longreasoningstring, andgoverningRulewith fullruleText. This is not a 42-character address in a 2^160 space that a dictionary attack can enumerate; it is a large document with high-entropy fields.
So the honest position is: the anchored roots are, at worst, pseudonymised personal data whose re-identification is computationally infeasible without the preimage, and are arguably anonymous in the hands of the anchoring parties (who hold no other information and have no reasonable means to obtain any — the Breyer / EDPS v SRB "reasonable means" test, applied to the recipient). That is a defensible Chapter V position for the digest legs, and a defensible answer to "you published my data to Bitcoin forever."
Does erasure work?
Crypto-shredding is the correct strategy and it is architecturally available — but the product does not implement, document or expose it.
The theory: delete the preimage (the record body held by VDA and in the customer's seal-journal.json), and the anchored root becomes an unlinkable, uninvertible 32-byte value. The data subject's personal data is effectively erased; what remains is a number that cannot be connected to them by any party. Several European supervisory authorities and the EDPB's 2025 blockchain guidance treat this family of argument as the viable route for immutable ledgers, provided only digests were ever published.
What is missing to actually execute it:
| Required | Present? | Where it would go |
|---|---|---|
| A delete API on the SaaS | No — WitnessClient has no delete/redact method (src/seal/witness-client.ts:103-416) |
New endpoint + client method |
| A local purge | No — FileSealJournal never evicts (src/cli/state.ts:98-156) |
witness-dao state purge |
| A retention policy or TTL | No — no such concept in WitnessDaoConfig (src/core/types.ts:339-360) |
Config + SaaS-side enforcement |
| Documentation of the shredding argument | No — zero mentions | DATA-PROTECTION.md |
| A statement of what survives erasure | No | Same |
What erasure would do to the chain
This is the technical objection, and it is smaller than it looks. Records are hash-chained per chainKey via prevHash (src/core/types.ts:138-153), and the offline verifier walks continuity (src/verify/offline.ts:555-600). Deleting a record's body while retaining its bodyHash, prevHash, seq, signature and sealedAt leaves the chain fully verifiable — continuity checks compare prevHash against the predecessor's bodyHash (:592), which is retained. Only the body-hash self-consistency check (:555-566, recomputing the digest from the body) becomes uncheckable, and the verifier already has a state for that: INSUFFICIENT_PROOF, not BROKEN. The Merkle inclusion proof against the anchored root still verifies, because it operates on bodyHash (src/anchor/anchorer.ts:614-618).
So a tombstoned record — hash retained, body erased — degrades gracefully: the chain still proves that a record with that digest existed at that position at that time, sealed by that key, anchored in that window. It simply no longer proves what it said. That is an entirely coherent evidentiary outcome, and it is exactly what a court would expect after a lawful erasure. The architecture supports tombstoning; the product just does not offer it.
Downstream reports are a different matter: a report generated before erasure has already been rendered and its reportDigest (src/report/governance.ts:1252-1256) quoted; an artefact in counsel's hands cannot be recalled. Art. 19 communication-to-recipients would have to be handled contractually and procedurally, not technically.
Art. 17(3) — does the retention exemption cover this?
(Analysis.) This is the strongest card the product holds, and it is a good one.
- Art. 17(3)(e) — retention necessary "for the establishment, exercise or defence of legal claims" — is precisely the product's stated purpose. The liability-defence pack exists for no other reason. Post-Ooki and post-Lido, the prospect of claims against DAO participants is concrete rather than speculative, which is what the exemption requires. Records concerning a person who was a multisig signer or an active voter, retained for the duration of the relevant limitation period, look like a textbook (e) case.
- Art. 17(3)(b) — compliance with a legal obligation — is weaker but available where the wrapper has a statutory record-keeping duty (a MiCA-authorised CASP under Art. 68, an AML-obliged entity under AMLD5/6 with its five-year retention, or corporate record-keeping under Wyoming DAO LLC law).
But the exemptions are narrow, purpose-bound and evidence-hungry. They will not cover: retention beyond the applicable limitation period; the free-text proposal excerpts (C-04), which are not necessary to establish who did what; data about voters who are not plausibly connected to any claim; or continued retention once the claim is resolved and time-barred. A blanket "we keep everything forever because litigation" will not survive scrutiny.
To rely on Art. 17(3)(e) the customer needs, in writing, before the first seal: the specific claim types anticipated; the applicable limitation periods per jurisdiction (Wyoming, RMI, N.D. Cal., relevant EU states) with a derived retention period; a data-category-by-category necessity analysis (which fields are necessary to defend a claim — addresses and votes, plainly; a 4,000-character proposal excerpt, much less obviously); a review trigger and a deletion procedure for when the basis lapses; and a documented response template for erasure requests explaining the refusal and the Art. 21 objection route.
Conclusion on erasure
The erasure problem is real but not fatal, and it is much better than the "anchored to Bitcoin forever" framing implies — because only digests leave, and the plaintext lives in exactly two erasable places (VDA and the local journal). The blocker is not the immutability of Bitcoin; it is that the vendor has built no deletion path, no retention concept and no documented crypto-shredding position for the one layer where deletion is entirely possible. Build tombstoning plus a retention policy and document the Art. 17(3)(e) reliance, and this becomes a defensible architecture. Ship it as-is and the honest answer to "erase my data" is "we cannot and we have never thought about it", which is not survivable in an EU enterprise procurement.
Over-claims register
Every claim the product makes that the implementation does not support. Ordered by materiality.
| # | Claim | Where | What the implementation supports | Severity |
|---|---|---|---|---|
| 1 | "AML/MiCA-facing reporting for the legal wrapper" | README.md:17; package.json keyword mica |
Nothing MiCA-specific; no AML capability at all. At best an evidence layer that could support the governance-arrangements limb of Art. 34. | High |
| 2 | "Upstream EU AI Act Article 12 evidence report" | src/seal/witness-client.ts:345 |
Art. 12 governs logging by high-risk AI systems. This product seals human governance actions. Category error. | High |
| 3 | "fast, legally-recognised (eIDAS) coverage" | README.md:68; src/anchor/index.ts:9; src/anchor/rfc3161.ts:2,6-8 |
Non-qualified TSAs. Art. 41(1) non-discrimination only; no Art. 41(2) presumption of accuracy. Chain validation is not_checked by default. |
Medium |
| 4 | complete_tally_sealed → PROVEN presented as an available tier |
README.md:143-149; src/report/liability.ts:33-36 |
Unreachable with any shipped provider — all tallies have actors: [] (src/binding/snapshot.ts:441), which assessTally correctly rejects (src/report/liability.ts:368-373). |
Medium |
| 5 | Governance rule asserts AML/sanctions results "SHALL be recorded" | governance-rules/treasury.transfer.md:10-12 |
No implementation. The rule text is sealed verbatim into records, so the DAO asserts a control the tool does not perform. | Medium |
| 6 | "Governance evidence that survives contact with a regulator, an auditor, or a court" | README.md:3 |
Evidentiary survival: plausible, and well engineered for. Data-protection survival: no — an EU regulator asking "what is your retention period and how do you handle an erasure request" gets no answer. | Medium |
| 7 | "Zero runtime dependencies" / "753 tests" | README.md:99, :170 |
Accurate as far as I checked (package.json dependencies: {}). But LIMITATIONS.md §1 concedes RFC-3161 CMS verification is untested against a live TSA — the one place a wire-format surprise could silently downgrade every anchor. Not an over-claim; noted because item 3 leans on this layer. |
Low |
| 8 | MCP tool: "PROVES: for participation, that the subject acted as recorded" | src/mcp/server.ts:513 |
Qualified immediately by "to the strength stated", and DOES NOT PROVE is spelled out for inferred_absence (:515-518). The word "PROVES" in an agent-facing description is still stronger than the calculus warrants where strength is WEAK. |
Low |
Not over-claims — checked and found sound: the PROVEN cell definition and its dependence on reading binding from signed inputs (src/report/strength.ts:36-52); the asserted_unverified Bitcoin verdict; the refusal to upgrade Snapshot tallies above api_asserted when truncated; the RECORDED-never-PASS verdict (src/seal/sealer.ts:296-308); assertCompliancePosture blocking unanchored records from reports (src/seal/witness-client.ts:418-445). These are, if anything, under-claimed.
What must exist before an EU enterprise pilot
Ordered. Items 1-6 are blocking; 7-12 are required before general availability.
- Data-protection position paper (
DATA-PROTECTION.md, vendor-produced) enumerating: every field sealed intodecision.inputsper provider (the C-03 table is a ready-made draft), every recipient (the C-06 table), the plaintext-vs-digest split, and the vendor's stated position that wallet addresses processed by this product are personal data. - Erasure and retention design, built not just documented: (a) a tombstone/redact capability on VDA that deletes the body and retains
bodyHash/prevHash/signature/seq; (b)witness-dao state purge --before <date>; (c) aretentionblock inWitnessDaoConfig; (d) a written crypto-shredding rationale explaining what survives and why it is no longer personal data. - DPIA — required on at least four EDPB criteria (evaluation/scoring, systematic monitoring, matching of datasets, effect on legal position). Must specifically address: irreversibility, Art. 9 inference risk (C-05), the third-party-voter problem (C-08), and the profiling capability (C-16). The vendor should supply a template pre-populated with the technical facts.
- Art. 28 DPA with VDA, with full 28(3) content, a named sub-processor list covering all seven anchor endpoints, change-notification, audit rights, and a deletion/return obligation on termination that is honest about what cannot be deleted.
- Chapter V package: VDA's processing location ascertained; SCCs (or DPF reliance) for the plaintext leg; a transfer impact assessment; and a separate short memo taking the position that digest-only transmission to OTS calendars, TSAs and Rekor either is not a transfer of personal data or is a transfer of effectively anonymised data — with the reasoning in the erasure section above.
- Retention schedule with limitation-period analysis, per jurisdiction, per data category, to support Art. 17(3)(e) reliance. Plus a documented erasure-request response procedure.
- Fix the three over-claims (register items 1, 2, 3): rewrite
README.md:17,README.md:68,src/anchor/index.ts:9andsrc/seal/witness-client.ts:345; drop or qualify themicakeyword. - Minimisation change: make free-text excerpt sealing opt-in, defaulting to digest-only; add a redaction hook on the provider interface (C-04). Consider the Merkle-committed actor-set idea from C-15.
- Append-correction:
supersedesin the sealed inputs, surfaced and excluded/annotated in both report builders (C-12). - Art. 30 record of processing and a layered privacy notice the customer can publish to the DAO forum, plus the Art. 14(5)(b) disproportionate-effort assessment.
- Purpose capture and access logging on report generation (C-16), plus the personal-data caveat on the MCP liability tool (C-18).
- Operational hardening already flagged by the vendor: durable DB-backed
SealJournal/AnchorRunStoreand the live-TSA integration test (LIMITATIONS.md§1). Both bear on evidentiary reliability and, via the journal, on where personal data sits.
Question set for counsel, consolidated: (i) confirm addresses-as-personal-data; (ii) confirm the digest-anonymity position for the anchor legs; (iii) confirm Art. 17(3)(e) reliance and the derived retention period; (iv) Art. 9 exposure per DAO subject matter and whether 9(2)(e) helps; (v) whether a DAO-as-general-partnership can be a joint controller and who signs; (vi) whether sanctions-screening dispositions are Art. 10 data; (vii) whether qualified timestamps matter for the target forums.
What is genuinely well done
Calibration matters, so: the evidentiary engineering in this codebase is of a standard I rarely see, and several of the design decisions are ones I would cite as exemplary.
- The binding-confidence ladder is the right abstraction, and it is enforced rather than described. "A sealed record is only as truthful as the claim fed into it — sealing an unverified assertion produces a beautifully tamper-evident lie" (
src/core/types.ts:5-8) is the single most important sentence in the repository, and the code honours it: no layer upgrades a confidence level, Snapshot tallies stayapi_assertedunless independently recomputed, and divergence from the API's own numbers is recorded rather than hidden (src/binding/snapshot.ts:380-409). - Every substantive report claim is read out of the signed body.
extractSignedEventView(src/report/governance.ts:366+) reads actors, kind, time, binding and detail fromrecord.decision.inputs— the bytes the signature and anchor cover — and treats a caller-supplied event as corroboration only, reporting divergence as a caveat. The comment at:10-14explains that before this was true, "the signatures were decorative with respect to what the report said". That is a class of bug most products ship with permanently. - Determinism and
reportDigest. Reports are stable-sorted with an injected clock and RFC 8785 canonical JSON, so a third party can be handed a digest and re-derive the artefact offline with no vendor involvement (src/report/governance.ts:15-20,:1252-1256). For admissibility and chain of custody this is worth more than most of the cryptography. - The disclaimer is architecturally non-weakenable, not merely present. Single-sourced, versioned, prepended verbatim, appendix-only extension, seven substantive clauses, regex defence against negation, and
assertDisclaimerIntactso a consumer can check a report it did not generate (src/report/disclaimer.ts:39-216). Clause 1 (evidence not certificate), clause 2 (attestation is a human act), clause 3 (not legal advice) and clause 6 (absence bounded by completeness) are exactly the right four, and the note at:21-25— that the disclaimer deliberately does not characterise case law because that would edge toward legal advice — shows genuine understanding of where the line is. - The absence calculus is legally coherent, which is the hardest thing here. You cannot prove a negative from an incomplete record set, and the code says so in the doc comment (
src/report/liability.ts:22-45), enforces it through three graded bases with hard ceilings, capsenumerated_votes_sealedatSTRONGspecifically because no sealed statement asserts the enumeration is complete (a subtle and correct distinction), and attaches a verbatim caveat to every inferred finding (:135-136).assessTallyrefuses a tally that declares itself incomplete, enumerates nobody, or whose declared count disagrees with its actor list (:349-390). Handed to counsel, this reasoning holds up: it does not claim to prove a negative, it states precisely what establishes completeness, and it degrades honestly when nothing does. My one reservation is C-15 — the strongest tier is unreachable with shipped providers — which is a product gap rather than a reasoning flaw. assertCompliancePosture(src/seal/witness-client.ts:418-445) structurally blocks unanchored Sealed-tier records from reports, and the reasoning ("that tier is terminal... presenting it as compliance-grade would be a material misrepresentation") is exactly right.- Honest failure states throughout:
asserted_unverifiedrather than a flattering guess on Bitcoin;not_checkedrather than a false pass on an unusable TSA certificate; refusing to evaluate keccak-256 attestations rather than guessing at SHA3 padding;RECORDEDrather thanPASS; exit codes 3 and 4 split to distinguish tampering from missing data. LIMITATIONS.mditself. A vendor document that opens "this file exists because the product's entire value proposition is not over-claiming" and then genuinely lists the blocking issues is rare and valuable. The data-protection gap this audit describes is, in a sense, the one thing it forgot to be honest about — and the fix is to extend the same discipline to a domain the authors clearly had not considered.
The gap this audit identifies is therefore not a failure of rigour. It is a rigorous team applying its rigour to evidentiary truth and cryptographic honesty, and not yet to data protection. Those are separable problems, and the second one is the smaller of the two.