{"slug": "brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain", "title": "Brick #1: Any Node Can Hand Off — Typed Participants in a Verifiable Supply Chain", "summary": "A project in the Bedrij verifiable supply-chain series shipped Brick #1, which adds four optional typed participant role slots — custodian, operator, device and sensor — to the existing handoff event payload, letting organizations, agents and devices travel in a single primitive that the replay read model surfaces per hop with rolesAssertedBy provenance. The roles are self-declared advisory metadata across seven address types (org, agent, device, robot, drone, service, human) and are never consulted by any authorization or payment guard, and the release followed an adversarial AI audit that found 0 Critical and 4 High issues, all fixed and re-verified. Events written before roles existed replay unchanged because every field is optional, and no new endpoint or contract was added.", "body_md": "# Brick #1: Any Node Can Hand Off — Typed Participants in a Verifiable Supply Chain\n\nA handoff event says custody moved from one address to another — but not WHO those addresses are. Brick #1 adds typed participant roles (custodian/operator/device/sensor) to the handoff payload: orgs, agents and devices in a single primitive, replayed with provenance. Includes the adversarial AI audit story — 0 Critical, 4 High findings, all fixed and re-verified.\n\nParticipant roles are optional typed declarations of who participated in a hop — custodian, operator, device, sensor — carried in the handoff event and replayed into per-hop state with rolesAssertedBy provenance. Self-declared, never identity: no guard consults them. Shipped after an adversarial AI audit (4 High findings, all fixed).\n\n*Part of the verifiable supply-chain series — start here: [The Bedrij Principle](https://agentbadge.xyz/blog/bedrij-principle). The hypothesis under test: a complex economic system can be composed from verifiable state transitions.*\n\nHere is the smallest broken thing in a supply chain: a `handoff` event that says *custody moved from 0xAA… to 0xBB…* — and nothing else.\n\nWhich of those addresses is a company? Which is a forklift's telematics unit, a dock-scheduling agent, a cold-chain probe? The ledger knows custody moved. It does not know **who** held the box — org, agent, or device. If the answer to that question lives only in somebody's spreadsheet, the chain of custody is verifiable and meaningless at the same time.\n\nThis is Brick #1 — the first experimental proof inside the Bedrij-series hypothesis. It proves a narrow claim: *transitions can be performed by nodes of any type, in a single primitive.* Orgs, agents and devices now travel the same `handoff` event, and the read model replays them with provenance. No new endpoint, no new contract — four optional slots on a payload that already existed.\n\n## What are participant roles in a verifiable supply chain?\n\n**Short answer:** participant roles are optional, typed declarations of *who participated in a hop* — `{ custodian, operator, device, sensor }` — carried inside the `handoff` event payload and surfaced by the replay read model. Each role is an address, one of seven types (`org | agent | device | robot | drone | service | human`), and an optional display label. They are self-declared by the party doing the handoff — advisory metadata, not identity, and never consulted by any authorization or payment guard.\n\nThe distinction matters more than it looks. Our custody model is enforced by addresses: `counterparty` must equal the plan's receiver, `actor` must equal the current custody holder. Roles deliberately sit *above* that — they describe the real-world shape of the participants without pretending the chain verified it. A forklift can be listed as `device` and a scheduling service as `agent`; the ledger records who claimed it, not who approved it.\n\n## How do roles travel through a handoff event?\n\n**Short answer:** roles enter at write time (`handoffHop(…, roles)`), are validated by a strict write schema, ride inside the `handoff` event's payload, and are lifted into `ShipmentHop.roles` during replay — so `GET /api/ledger/shipments/:id` returns them per hop with `rolesAssertedBy` naming who made the claim. Events written before roles existed replay unchanged: every field is optional.\n\nThis is the actual replay output from our test pack — two hops, one with a company custodian plus device and sensor, one with a custodian and an agent operator (addresses shortened for readability):\n\n```\n{\n  \"shipmentId\": \"shp-brick1-demo\",\n  \"hops\": [\n    {\n      \"hopIndex\": 0,\n      \"status\": \"paid\",\n      \"receiver\": \"0xbbbb…bbbb\",\n      \"roles\": {\n        \"custodian\": { \"address\": \"0xbbbb…bbbb\", \"type\": \"org\", \"label\": \"TransMar Logistics\" },\n        \"device\": { \"address\": \"0xcccc…cccc\", \"type\": \"device\", \"label\": \"Forklift-7 telematics\" },\n        \"sensor\": { \"address\": \"0xdddd…dddd\", \"type\": \"device\", \"label\": \"Cold-chain probe\" }\n      },\n      \"rolesAssertedBy\": \"0xaaaa…aaaa\"\n    },\n    {\n      \"hopIndex\": 1,\n      \"status\": \"handed_off\",\n      \"receiver\": \"0xeeee…eeee\",\n      \"roles\": {\n        \"custodian\": { \"address\": \"0xeeee…eeee\", \"type\": \"org\", \"label\": \"Eindhoven Cold Hub\" },\n        \"operator\": { \"address\": \"0xcccc…cccc\", \"type\": \"agent\", \"label\": \"Dock-scheduler agent\" }\n      },\n      \"rolesAssertedBy\": \"0xbbbb…bbbb\"\n    }\n  ]\n}\n```\n\nThree things worth reading in that JSON. The same address `0xcccc…` plays `device` on hop 0 and `agent` on hop 1 — a type is a claim about a role in *this* hop, not a global identity. `rolesAssertedBy` moves: the client asserted hop 0's roles, the carrier asserted hop 1's. And hop 1's handoff works even though hop 0 already settled — roles are per-transition data, not per-shipment configuration.\n\n*The lifecycle of a role claim: asserted at write time by the party doing the handoff, validated by a strict schema, stored in the append-only log, then lifted by a lenient replay into per-hop state — with rolesAssertedBy recording who made the claim. No guard ever consumes it: custody still answers only to counterparty and actor.*\n\n## Why do self-declared roles need provenance?\n\n**Short answer:** because an unverified claim displayed as structured truth is indistinguishable from a verified one. A hander-off could label any third party \"Maersk\" and a consumer reading only `hops[].roles` would never know it was self-reported. We fixed that by writing the *asserting party* into the derived model — `rolesAssertedBy` — and by enforcing that `custodian.address` must equal the handoff `counterparty`, since custodian is the slot whose meaning the ledger already defines.\n\nThat fix came from an adversarial review, not foresight. We ran the changes through an AI audit before shipping: **0 Critical, 4 High, 6 Medium findings** on the first pass. The Highs were exactly the trust-boundary bugs you'd want caught — roles presented as structured truth with no provenance (H-1), `custodian` able to contradict the custody model (H-2), a repeat handoff silently rewriting roles (H-3), and an `epcis` object able to smuggle keys that overrode guarded payload fields (H-4). All four are now guarded, and the re-audit came back **APPROVE WITH NITS**.\n\n## What did the audit actually change in the code?\n\n**Short answer:** four behavioral changes and a schema split. Write-side parsing is now strict — unknown role slots or keys fail loudly instead of silently stripping; addresses normalize to lowercase; `label` rejects control, bidi and zero-width characters and must be non-blank; `custodian` must equal the counterparty; a hop already handed off rejects re-handoff; and the payload's guarded fields (`shipmentId`, `hopIndex`) are written last so a caller-supplied object can't overwrite them. The read schema stays lenient — stored events always replay.\n\nThe schema split is the part I'd highlight for anyone building on append-only logs: the *write* schema and the *read* schema are now different objects. `ParticipantRolesWriteSchema` is strict (`z.strictObject`, normalization, consistency refines). `ParticipantRoleSchema` — the one inside `HandoffPayloadSchema` and `ShipmentHopSchema` — is deliberately lenient, because tightening a rule tomorrow must never make a stored event unparseable. An append-only log means your read contract can only grow more permissive over time.\n\nCurrent state: 76 package tests and 40 server tests green, zero open Critical/High findings, two audit reports committed alongside the code.\n\n## What roles are NOT — the honest boundary\n\n**Short answer:** roles are not a registry, not a delegation mechanism, and not a signature scheme. Nothing checks that \"Eindhoven Cold Hub\" is who it says it is; `rolesAssertedBy` is only as strong as the authentication on the write path; `label` is display text and a determined attacker can still craft look-alike strings. If your application needs *verified* participant identity, roles are the wrong layer — they're the substrate such a verification layer would annotate.\n\nThat's deliberate. Brick #1 proves the *shape* of the claim — \"transitions can name nodes of any type\" — not the verification of the claim. The interesting follow-ups are exactly the missing layers: a participant registry, countersigned roles (where the named party must consent), and device attestations. Those are different bricks.", "url": "https://wpnews.pro/news/brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain", "canonical_source": "https://agentbadge.xyz/blog/supply-chain-participant-roles", "published_at": "2026-10-29 00:00:00+00:00", "updated_at": "2026-11-12 22:53:29.605695+00:00", "lang": "en", "topics": ["ai-agents", "artificial-intelligence"], "entities": ["Bedrij", "Brick #1", "TransMar Logistics", "Forklift-7", "ShipmentHop.roles", "rolesAssertedBy"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain", "markdown": "https://wpnews.pro/news/brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain.md", "text": "https://wpnews.pro/news/brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain.txt", "jsonld": "https://wpnews.pro/news/brick-1-any-node-can-hand-off-typed-participants-in-a-verifiable-supply-chain.jsonld"}}