Independent AI agent for EU Deforestation Regulation compliance, not affiliated with the EU. Scope & disclaimer →
eudr.dev

Compliance audit · eudr-a2a

Last audited 2026-10-11 against the primary sources below. Every claim in this document is a code reference (file:line) and a regulation citation.

Earlier passes are dated in the audit log at the foot of this page. The 2026-10-11 pass re-read Art. 3 end to end against the code and found the Art. 3(b) aggregate wrong; what it changed is in that log.

Sources

Annex II. Fields the DDS carries

Annex II itemRegulation text (short)Engine fieldCode
1Operator's name, address, and EORI (per Reg. 952/2013 Art. 9)1_operatorAnnexII::operator
2HS code, free-text description (incl. trade name, scientific name where applicable), and quantity (kg + supplementary unit where applicable)2_productProductDescriptor
3Country of production + geolocation of every plot3_geolocation (Vec\<Geolocation\>)Geolocation
~~4~~~~Upstream-DDS reference~~ deleted by 2025/2650absentAnnexII — comment block confirms intentional absence
5Verbatim declaration text5_declarationANNEX_II_VERBATIM_DECLARATION; byte-exact, 287 bytes
6Signature block (signed-for-and-on-behalf-of / date / function / signature)6_signatureSignature; ed25519 base64 + DID + pubkey. key_held_by names who holds the signing key, so the block never reads as though a person signed what a machine signed. The operator's own sign-off is a separate record (OperatorSignOff, POST /app/api/orgs/:org/shipments/:id/sign-off).

The engine emits these as JSON keys exactly as written, so an auditor can hold the regulation open next to the JSON and read top-to-bottom.

Articles. What the engine enforces and where

Art. 2(13). Cut-off date

"deforestation-free" means produced on land not subject to deforestation after 31 December 2020

WhereWhat
CUT_OFF_DATE"2020-12-31" constant
runner.rsPassed verbatim to emem's /v1/eudr_dds request as cut_off_date
Report"CUT-OFF 31 Dec 2020" tag in the tags strip; cut-off vertical line on field-history timeline

Art. 2(28). Geolocation precision and polygon rule

"geolocation" means the geographical location of a plot of land described by means of latitude and longitude coordinates corresponding to at least one latitude and one longitude point and using at least six decimal digits [..] for plots of more than 4 hectares used for the production of the relevant commodities other than cattle, polygons [..]. For cattle, the geolocation refers to all establishments [..]

RuleCodeStatus
≥ 6 decimal digits required on every coordinatevalidate_plot_geometryEnforced. Server returns 400 with the exact precision reported.
Cattle requires polygon regardless of areaCommodity::polygon_always_required consulted by validate_plot_geometryEnforced. Cattle + Point → 400.
Non-cattle plots > 4 ha must be polygonsring_area_ha computed; Point geometries are accepted under that areaPolygon shape is the operator's responsibility above 4 ha; the validator records area_ha so the DDS document carries it.

Test: art_2_28_rejects_under_6_decimal_precision. Test: art_2_28_cattle_rejects_point_geometry. Sample data: every samples/scenarios/.json polygon now meets the 6-digit minimum.

Art. 3. Prohibition on placement

Relevant commodities and relevant products shall not be placed on the market [..] unless [..] (a) [..] deforestation-free; (b) [..] in accordance with the relevant legislation of the country of production; (c) [a DDS] has been made available [..].

WhereWhat
runner.rsWhen verdict is NonNegligible, the DDS document carries a non-attestation declaration instead of the Annex II point-5 wording, and the schema id is suffixed .assessment so downstream verifiers can refuse to forward it as a DDS.
ReportWhen verdict is non-negligible the printable report renders "Don't ship yet" + an explicit Art. 4(1) sentence.
legality_assessmentsArt. 3(b) is the operator's assessment, not a satellite's. The operator records one per supplier — assessor, function, date, validity, and the documents read, at least one required — and the compile reads it from the organisation's own records at the choke point. A value supplied in the request is dropped.
ConclusionCarries article_3a_deforestation_free and article_3b_legality as two separate findings, because they are two separate conditions and a document that merges them hides which one is unmet.
Stage modelneeds_legality is its own stage. Without a recorded assessment a statement is indeterminate and says what to record; Art. 13 simplified due diligence is evaluated after the legality gate, not instead of it.

This avoids the false-attestation trap: a signed document with the verbatim "no or only a negligible risk was found" wording over a non-negligible verdict would be a literal false statement.

Found 2026-10-11. legally_produced aggregated the legality findings as all(status != Failed), so an unresolved check counted as a clear one — and the tenure-and-permits check is unresolved on every statement by construction, because no satellite reads a land registry. Every statement the engine had ever issued therefore said the goods were legally produced, beside a land_tenure_and_permits: unknown of its own. The aggregate now requires every applicable finding to be Verified, which is what the field's own doc comment had always said.

Test: composite_risk tests cover every tier × fail-fraction combination including the high-risk floor.

Art. 4(1). Operator must not place product

The runner emits PipelineEvent::Blocked on non-negligible verdicts. The CLI and server present this state and refuse to mark a task paid on the assessment-only schema.

Art. 4(2). DDS reference with the customs declaration

The scenario carries an optional production.customs_declaration_ref (MRN or local reference). The report's Customs & Shipping (3b) section surfaces this field; the engine does not pre-validate the MRN format (operators in different member states use different formats).

Art. 4(3). Five-year retention

Operators shall keep [..] the due diligence statements they have submitted [..] for a period of five years from the date of submission.

WhereWhat
RECORD_RETENTION_YEARS = 5Constant surfaced in the report footer and Art. 12 panel
EUDR_TASK_RETENTION_DAYS env (default 7)In-process working-memory window. The 5-year archive is the operator's responsibility.
DELETE /a2a/tasks/:idClears scenario + DDS payload but preserves the receipt CID + audit aggregate (Art. 17(3)(b) GDPR).

Art. 8. Three steps of due diligence

StepEngine stage
Art. 8(2)(a) — information collection (Art. 9)geo + forest stages
Art. 8(2)(b) — risk assessment (Art. 10)legality + risk stages
Art. 8(2)(c) — risk mitigation (Art. 11)The engine signals required mitigation in the report's Article 11 panel; the act of mitigating belongs to the operator.

Art. 9(1). What the operator collects

Coverage matrix is surfaced in the report as the "Article 9(1) · information collected" panel.

Sub-articleField on ScenarioEngine behaviour
(a) description, type, scientific nameproduct.description, product.trade_name, product.scientific_nameDescription and HS required; trade name + scientific name optional but recommended
(b) quantity (kg + supplementary unit where applicable)product.quantity_kg, product.supplementary_unit, product.supplementary_quantityQuantity > 0 enforced
(c) country (and sub-region where relevant) of productionproduction.country, production.country_subregionCountry required
(d) geolocation of every plot + date or interval of productionproduction.plot_geojson + additional_plots; production.harvest_windowRequired; ≥ 6 decimals enforced
(e) supplier name, postal address, emailsupplier.name, supplier.address, supplier.emailName + ID required; address + email optional but the report flags absence in the Art. 9 coverage matrix
(f) downstream recipient details (when applicable)Out of scope (operator is the recipient on first placement)Coverage matrix marks (f) with a contextual note
(g) deforestation-free evidence (conclusive + verifiable)source_receipts.emem.per_cell[].fact_cidsEvery per-cell verdict carries fact CIDs; signed receipt envelope verifies offline
(h) lawful production evidencelegality.findingsBaseline runs country-tier + supplier-id checks; land tenure and harvest permits remain in scope for a partner provider behind LegalityProvider

Art. 9(1). Five-year retention

Same five-year horizon as Art. 4(3). The receipt CID stays as the audit anchor even after operator-side erasure (Art. 17(3)(b) GDPR).

Art. 10. Risk assessment

Reported in the report's "Article 10 · risk assessment" panel. Criteria (a)–(k) listed with what the engine assessed and what it did not. The composite-risk function is exposed and tested:

composite = clamp(fail_fraction × 0.85 × tier_mult, 0, 1)

Test: 8 cases in composite_risk tests.

Art. 10(2)(j). Risk of mixing / substitution

Operator-declared third-party certifications (RSPO, FSC, RA, Fairtrade, ISCC_EU, PEFC, …) are recorded in scenario.certifications and rendered in the report. They are surfaced in the Art. 10 row as mitigation evidence, not a substitute for due diligence.

Art. 11. Risk mitigation

The report's Article 11 panel is verdict-aware:

Art. 11 measures listed, plus a note that the report must NOT be submitted as a negligible-risk attestation.

Art. 12. DD system + Art. 4(2) submission

The report's Article 12 panel records the engine identity (eudr-a2a, vortx-ai/eudr), notes annual reporting under Art. 12(3), and flags submission status:

TRACES SOAP submission is on the roadmap; the engine output is otherwise submission-ready.

Art. 13. Simplified due diligence

simplified_dd_applied is set true only when the production country is on the Implementing Reg. 2025/1093 low-risk list. The flag is recorded in conclusion.simplified_dd_applied and surfaced as a tag on the report.

Art. 29. Country benchmarking

The country tier table is data/country_benchmarks_2025_1093.json, loaded at startup. High-risk (4): BY, MM, KP, RU. Conservative low-risk list (~140). Anything unlisted resolves to STANDARD.

Art. 38 (replaced by 2025/2650). Application dates

GroupApplication dateConstant
Default (most operators + medium-sized undertakings)30 December 2026APPLICATION_DATE_DEFAULT
Natural persons + micro / small operators established by 31 Dec 2024 (FLEGT-Annex products excluded)30 June 2027APPLICATION_DATE_DEFERRED_MICRO_SMALL_NATURAL

Surfaced in the agent card's /spec page on the public console.

Signed proof

PropertyValue
Algorithmed25519 over canonical JSON bytes
CIDCIDv1, raw codec (0x55), BLAKE3-256 multihash
CanonicalisationHand-implemented JCS-style: sorted keys, no insignificant whitespace
Round-trip safetycanonical_b64 field carries the exact bytes that were signed and hashed, so JSON re-serialisation cannot break verification
Verifiereudr verify <path> (offline); POST /api/verify_receipt (online via emem)

Tamper detection test: tamper_detected.

TRACES NT submission output

Three formats, one CLI:

eudr compile samples/scenarios/civ-cocoa.json --out civ.json

# Engine-annotated XML (verbose; namespace urn:vortx-ai:eudr-a2a)
eudr export-traces civ.json --format engine --out civ.engine.xml

# EU Commission TRACES NT v2 shape. The wire format the Information System accepts
eudr export-traces civ.json --format official --activity IMPORT --out civ.official.xml

# Full SOAP envelope with WS-Security UsernameToken-Digest, ready to POST
EUDR_TRACES_USERNAME=service@deployer.eu EUDR_TRACES_PASSWORD=… \
eudr export-traces civ.json --format soap --activity IMPORT --out civ.soap.xml

civ.official.xml namespace: http://ec.europa.eu/tracesnt/certificate/eudr/submission/v2. Root element: <eudr:SubmitStatementRequest>. Field names match the EU Commission's v2 API spec (operatorType, statement.activityType, statement.countryOfActivity, statement.commodities[].hsHeading, statement.commodities[].producers[].geometryGeojson base64-encoded, operator.operatorAddress.fullAddress, …).

civ.soap.xml adds a SOAP 1.1 envelope with the WS-Security header: UsernameToken with PasswordDigest = base64(SHA1(nonce ‖ created ‖ password)), fresh 16-byte CSPRNG nonce, RFC-3339 UTC Created timestamp. TRACES NT's EUDRSubmissionServiceV2 endpoint accepts this verbatim.

civ.engine.xml is what we already emitted. Useful for archival and internal audit because every <EngineConclusion> and source-cited element is namespaced separately from the official submission shape.

Continuous monitoring (Art. 8(2))

# A2A SendMessage on the new skill (advertised in the agent card):
curl -X POST https://eudr.dev/a2a/rpc \
 -H 'content-type: application/json' \
 -d '{"jsonrpc":"2.0","id":"r","method":"SendMessage",
 "params":{"task_type":"eudr.monitor.subscribe",
 "input":{"dds_reference":"eudr-a2a:engine:CI:…","cadence":"monthly"}}}'

The agent re-runs the deforestation check on the cadence, when a new Hansen loss year appears post-cut-off for any sampled cell, the subscription emits a SignedReceipt-shaped event citing Art. 8(2). The first re-check after each annual Hansen update is free; later checks are priced per call.

civ.xml is a namespaced XML carrying every Annex II point that TRACES NT requires:

<DueDiligenceStatement
 xmlns="urn:vortx-ai:eudr-a2a:dds:v2025_2650"
 schema="eu.eudr.dds.v2025_2650"
 submission_status="pre_submission">
 <Header>..</Header>
 <Operator>..<EORI>..</EORI>..</Operator> <!-- point 1 -->
 <Product><HsCode>..</HsCode><Cn8>..</Cn8>..</Product> <!-- point 2 -->
 <Geolocation><Plot index="0">..</Plot>..</Geolocation> <!-- point 3 -->
 <!-- point 4 deleted by Reg. (EU) 2025/2650 -->
 <Declaration verbatim="true">..</Declaration> <!-- point 5 -->
 <Signature>
 <Name>..</Name><Place>..</Place><Date>..</Date>
 <Function>..</Function>
 <CryptographicProof>
 <Algorithm>ed25519</Algorithm>
 <SignatureBase64>..</SignatureBase64>
 <PublicKeyBase32>..</PublicKeyBase32>
 <SignerDid>..</SignerDid>
 </CryptographicProof>
 </Signature> <!-- point 6 -->
 <EngineConclusion>..</EngineConclusion>
</DueDiligenceStatement>

submission_status is pre_submission for filable, not_filable when the schema is .assessment, draft when the document is unsigned. A production TRACES adapter replaces the engine namespace with urn:eu:eudr:dds:v1, wraps the document in the official SOAP envelope, and POSTs to https://eudr.webcloud.ec.europa.eu/tracesnt/. The <EngineConclusion> block is dropped on submission (it is the engine's audit annex, not part of Annex II).

What the engine does NOT do

Documented openly so an auditor can see the boundary:

  1. Art. 9(1)(b). Lawful production. emem.dev's own response carries

a legality_disclaimer saying production-law checks are structurally outside Earth-observation scope, and that is still true: nothing here reads a land registry, a harvest permit or a record of free, prior and informed consent. What changed on 2026-10-11 is who answers. The engine no longer guesses from a country tier; the operator records their own Art. 3(b) assessment against the supplier, naming the assessor, the documents read and how long it holds, and the statement carries that verbatim. Without one, the statement is indeterminate. A partner legality provider can still sit behind the LegalityProvider trait; it is no longer the only way to reach a filable statement. The report's Article 10 panel marks (c)–(e), (j) and (k) as "Not assessed by this engine."

  1. TRACES NT submission. The engine produces a submission-ready

document. The SOAP envelope and CIRCABC credential flow are on the roadmap.

  1. HSM-backed signing in production. The bundled dev key is

deterministic and useful only for development. The server refuses to boot with the dev key over HTTPS. Production deployments must set EUDR_AGENT_PRIVKEY_B64 to a real 32-byte secret (ideally sourced from an HSM via KMS).

  1. Replacement for the operator's compliance programme. The

regulation places the obligation on the operator. The engine is a processor that runs steps required by Art. 8(2) and emits a signed record. The operator retains responsibility for verifying inputs and for the final submission decision.

Audit log

DateAuditorFindingFix
2026-10-11self-auditArt. 3(b). legally_produced was all(status != Failed), so an unresolved check counted as a clear one. Every statement the engine had issued said the goods were legally produced, beside its own land_tenure_and_permits: unknown.The aggregate requires every applicable finding Verified. New legality_assessments table and API: the operator records the assessment per supplier with assessor, function, date, validity and documents; the compile injects it server-side and drops any value in the request. New needs_legality stage; Art. 13 moved after the gate.
2026-10-11self-auditAnnex II point 6. The operator block arrived with the request: the console overwrote the name but fell back to request-supplied EORI, and the agent surface overwrote nothing. One account could sign a statement naming another company.name_the_operator() takes identity from the organisation record only; an unset field cannot be supplied by the request (409). Anonymous compiles must name a bundled demo operator. Added 6_signature.key_held_by and a separate operator_sign_off record.
2026-10-11self-auditArt. 9(1)(d) proxies. Four pack rules, including "outside protected areas", returned PASSED from OpenStreetMap boundaries while the same page said the official registers had not been checked.HowChecked.establishes + CheckState::LooksClear: a proxy can no longer pass a rule. Amber, counted apart, still open.
2026-10-11self-auditArt. 4(3) / 12(5) retention. A statement's report, event stream and TRACES exports answered anyone holding the task id.statement_access.rs gates every read to a member or a labelled, expiring, revocable share link whose token is stored as a hash. Submit is members-only.
2026-10-11self-auditThe verifier page wrote every answer into a box it had just set to display:none. Valid, invalid and malformed receipts looked identical.Fixed; scripts/verify_page_check.py asserts the computed display of the result box rather than its text.
2026-05-17self-auditArt. 2(28) ≥ 6-decimal precision not enforced (sample data had 4 decimals; engine accepted)Added validate_plot_geometry rejecting < 6 decimals; regenerated every sample to 6-decimal coords; geom test threshold raised.
2026-05-17self-auditArt. 2(28) cattle-polygon rule not enforcedvalidate_plot_geometry rejects Point + cattle.
2026-05-17self-auditNon-negligible verdict still produced a signed Annex II document carrying the verbatim attestation ("no or only a negligible risk was found")Schema id now suffixed .assessment on non-negligible verdicts; declaration text replaced with a non-attestation explanation. The receipt schema matches so downstream verifiers can route or reject without parsing the payload.
2026-05-17self-auditPolygon-area Hansen post-cut-off loss could be missed by the 16-cell eudr_dds sampler and silently produce a "low risk" verdictReport's live land-cover panel now compares the polygon-area Hansen scan to the per-cell sampler and emits an amber warning when they disagree, citing Art. 8(2).
2026-05-17self-auditThreshold-based verdict (composite < 0.20 → negligible) ignored confirmed per-cell post-cut-off loss. Art. 2(13) is a binary condition.Runner now flips to NonNegligible when any per-cell entry has hansen_lossyear >= 21 (or >= 2021 full-year) OR a fail-class label. Threshold is now a secondary gate.
2026-05-17self-auditNo TRACES NT submission output. The PDF / JSON cannot be filed; the EU Information System accepts SOAP/XML only.Added eudr-core::traces::to_traces_xml + eudr export-traces CLI. Output is a namespaced XML carrying every Annex II field plus <EngineConclusion> + cryptographic proof. A TRACES adapter swaps the namespace for the official one and wraps in the SOAP envelope.
2026-05-17self-auditAnnex II point 6 missed the Place of signing. Regulation lists "Place" in the signature block.New Operator.signing_place (optional) + Signature.place. Defaults to country of establishment when absent. Rendered in the PDF signature block AND in the TRACES XML.
2026-05-17self-auditEORI format not validated. Any string was accepted.New is_valid_eori: ISO-alpha-2 + 1.15 alphanumeric (Reg. 952/2013 Art. 9). Server returns 400 with the exact failure mode.
2026-05-17self-auditPolygon GIS sanity (self-intersection, degenerate) not checked. A bow-tie polygon would pass shape + precision checks and produce an ambiguous verdict.check_polygon_simple runs OGC-Simple-Features pair-wise edge crossing + degeneracy check on every plot.
2026-05-17self-auditCN8 (8-digit Combined Nomenclature) not surfaced. EU customs declaration + TRACES require CN8; engine accepted HS6.TRACES XML carries both <HsCode> (operator-supplied) and <Cn8> (derived via zero-pad from HS≥4). Operator-supplied CN8 round-trips unchanged.
2026-05-17self-auditReport didn't disclose it's the audit record, not the legal submission.Cover page now has a .status-strip panel: submission status (filable vs assessment vs draft), document role (5-year audit record per Art. 4(3)/9(1)/12(5)), and the verification path (eudr verify, fact-CID dereference URL).
2026-05-17self-auditReport carried only the per-cell verdict matrix and a single satellite chip. No NDVI scalar with capture date, no SAR, no crop-calendar cross-check.Runner now does a second emem pass after eudr_dds: pulls indices.ndvi + sentinel1_raw across the polygon bbox. Each fact carries its Sentinel-2 / Sentinel-1 scene id + captured_at. Surfaced in a new "Sentinel-1 SAR + Sentinel-2 NDVI · single-snapshot per cell" panel with per-cell table, statistics, and the regulation-cited interpretation bands. Disclosed honestly: emem returns single-snapshot, not time-series.
2026-05-17self-auditNo reference cross-check for the operator-declared harvest window.New data/crop_calendars.json table sourced from FAO GIEWS, USDA FAS, ICCO, RSPO. New eudr-core::crop_calendar module performs cross-check per commodity × country and returns in_main_season / in_secondary_season / off_season / no_reference. Surfaced as a 12-month grid in the report comparing the declared window to the reference calendar. Five new tests covering Côte d'Ivoire cocoa, Brazil soy (main + off-season), and the no-reference fallback.

Test coverage

cargo test --workspace. 58 tests passing across the workspace.

Crate / fileTests
eudr-core::commodity — from_hs18
eudr-core::country — BenchmarkTable::tier (loads shipped JSON)6
eudr-core::eu — EU_MEMBER_STATES sanity5
eudr-pipeline::runner — composite_risk8
eudr-pipeline::geom — inspect (incl. civ-cocoa sample)4
eudr-receipts — sign / verify / tamper6
eudr-a2a::skills — skill catalog shape and prices5
eudr-server::routes::validation_tests — Art. 2(28) cases6

Verbatim declaration. Byte-exact check

The Annex II point 5 declaration text is mandated wording. The string in ANNEX_II_VERBATIM_DECLARATION is 287 bytes long and round-trips through the JSON serialiser unchanged:

By submitting this due diligence statement the operator confirms that due
diligence in accordance with Regulation (EU) 2023/1115 was carried out and
that no or only a negligible risk was found that the relevant products do
not comply with Article 3, point (a) or (b), of that Regulation.

This is the consolidated text from CELEX:02023R1115-20241226 Annex II, unchanged by Reg. 2025/2650.

Re-running this audit

cargo test --workspace
cargo run --bin eudr -- compile samples/scenarios/civ-cocoa.json --out /tmp/civ.json
cargo run --bin eudr -- verify /tmp/civ.json
python3 -c "
import json; d=json.load(open('/tmp/civ.json'))['payload']
print('Annex II keys:', sorted(k for k in d if k[:1].isdigit()))
print('Point 4 absent:', '4_' not in str(list(d.keys())))
for g in d['3_geolocation']:
 ring = g['plot_geojson']['coordinates'][0]
 md = min(min(len(repr(c).split('.')[-1]) for c in pt) for pt in ring)
 print(f'plot {g[\"cell64\"]}: precision={md} digits. Art. 2(28):', 'PASS' if md >= 6 else 'FAIL')
print('Verdict:', d['conclusion']['verdict'], '· schema:', d['schema'])
"

AI Act and product-safety posture (verified 2026-06-04)

Because this engine serves other AI agents (A2A v1.0 + MCP), not only humans, its own AI-regulation posture is part of the compliance surface:

rulestatuswhere enforced
AI Act (EU) 2024/1689 Art. 50(1) — disclose AI interaction (applies 2 Aug 2026)Implemented early. Human surfaces brand the service as an AI agent (obvious-from-context limb); the agent card carries a machine-readable ai_system_disclosure block for agent callers.crates/eudr-a2a/src/card.rs (AiSystemDisclosure); test guard agent_card_carries_ai_act_disclosure
AI Act role classificationDownstream deployer; not a GPAI model provider; not Annex III high-risk. No in-force obligation binds the service today.REGULATORY_CROSSWALK.md §Adjacent EU rules
GPSR (EU) 2023/988Out of scope — consumer-product regulation; this is a B2B operator/trader service with no consumer nexus.legal/terms.md §2 (B2B scope)
MDR 2017/745 Annex I ("GSPR")Out of scope — medical devices only. Listed to disambiguate the acronym.—

Human oversight invariant: a non-negligible or indeterminate verdict is never auto-filed; TRACES submission is the operator's regulatory act under their own EU Login identity.

This page last changed on . It describes the Regulation as it stood then; where a date or a code matters to a decision, check it against the EUR-Lex text it cites.