# ADR-0001: Foundational protocol decisions Source: docs/adr/0001-foundational-protocol-decisions.md **Status:** Accepted **Date:** 31 August 2026 This tracked architecture decision record contains the foundational decisions that govern OpenFreytProtocol. Research is informative and cannot override accepted ADRs. The ignored root `DECISIONS.md` may serve as a local workflow projection, but this ADR is the durable source. ## D001 — Closed partner network first The first protocol profile supports a closed private exchange. Organizations exchange data only through explicitly accepted partner relationships. ADR-0002 extends the capacity-discovery foundation with tendering, commitment and execution handover. ## D002 — Narrow initial use cases Start discovery with recurring lanes, empty return capacity, residual capacity and compatible load requests. Public spot-market discovery, auctions, multi-leg optimization and automation of external legal contracts remain deferred. ADR-0002 adds operational tenders and protocol commitments without claiming that a protocol commitment replaces a legal transport contract. ## D003 — Neutral reference broker Use a logically neutral central reference broker for the first implementation. Freyt may implement or host it and acts as the first reference client, but every participant uses the same public protocol and no operation is exclusive to Freyt. ## D004 — Small project-owned canonical contract Define a narrow public JSON contract around partner relationships, capacity offers, load requests, match proposals, match decisions and execution handoff. Do not adopt a broad external logistics model as the canonical domain. Use: - JSON Schema 2020-12 for payload semantics; - OpenAPI 3.1 for synchronous HTTP operations; - CloudEvents for asynchronous event envelopes; - OAuth 2.0 and, where required, OpenID Connect for identity and authorization; - publicly available ISO, UN and IETF standards for primitive values. ## D005 — External formats remain adapters Existing business-document, EDI, visibility and TMS formats may be supported by adapters when a concrete participant needs them. They do not define the canonical model and are not a condition for participation. ## D006 — Avoid inaccessible or immature dependencies The protocol must not depend on closed specifications, membership-only contracts, difficult-to-consult documentation or insufficiently mature infrastructure. Any new external dependency requires an ADR with accessibility, maturity, licensing, tooling and exit-risk evidence. ## D007 — No mandatory federated infrastructure The first profile does not require a dataspace connector, federation platform or external certification framework. Purpose limitation, organization identity, explicit authorization and data sovereignty are implemented directly using mature internet standards. Federation can be evaluated later without changing core exchange semantics. ## D008 — Minimum disclosure Before the configured match phase, share only the corridor, time window, vehicle, capacity and compatibility information needed for discovery. Customer names, exact addresses, prices and commercially sensitive execution details remain protected according to bilateral policy. ## D009 — Explicit commitment A proposal is non-binding. A match becomes an operational commitment only through explicit authorized acceptance according to the bilateral decision policy. The protocol does not automatically form legal contracts, settle payments or award work without confirmation. ## D010 — Immutable lifecycle history Resources evolve through versioned transitions. Accepted decisions cannot be silently rewritten. Corrections append or supersede records and retain the reason, actor, timestamp and prior relationship. ## D011 — Implementation-independent conformance The specification and conformance suite define behaviour; the reference implementation does not. Stable releases require a reference broker, the Freyt client and at least one independently implemented client or simulator to pass the same black-box suite. ## D012 — Assurance-driven development Production implementation begins only after requirements, invariants, lifecycle models, authorization rules, threats and conformance checks exist. Critical protocol claims require traceable executable evidence. TDD, property-based testing, mutation testing, formal lifecycle modeling, real-boundary integration testing and resilience testing are required according to `QUALITY-STRATEGY.md`. ## D013 — Open governance Publish specifications, schemas, examples and conformance tooling under a permissive license. Manage normative changes through tracked ADRs or RFCs, immutable schema versions and compatibility checks. Prefer additive evolution within a major version. **Open point:** Confirm the legal entity or community structure that owns releases, trademarks and governance rights. ## D014 — Documentation is generated from protocol sources The static documentation website is part of the protocol repository. Every development and production build validates canonical content and regenerates documentation pages, structured-artifact reference pages, downloadable artifacts and machine-readable indexes from the same source revision. Generated pages are never edited as independent sources. Use an open-source static toolchain. Publishing to a public hosting target remains a separate operational decision. --- # ADR-0002: Private exchange and execution handover Source: docs/adr/0002-private-exchange-execution-handover.md **Status:** Accepted **Date:** 1 September 2026 This ADR expands the first protocol profile from capacity discovery into a complete private exchange between heterogeneous transport management systems. It supplements ADR-0001 and does not turn OpenFreytProtocol into a universal transport model. ## D015 — Separate discovery from execution transfer Capacity discovery, load discovery and match proposals remain independent from transport tendering and execution handover. A match may lead to a tender, but a match is neither an operational assignment nor a legal commitment. ## D016 — Transfer a bounded execution package The protocol transfers a vendor-neutral execution package containing only the stops, time windows, cargo facts, constraints, references, instructions and disclosure-controlled contact details needed to perform an accepted movement. It does not copy a source system's internal ride, order, shipment, route or planning object. The source and executing systems retain their own local records. The exchange links those records through the protocol resource identifier and namespaced external references. ## D017 — Distinguish agreement from technical handover Commercial acceptance and technical import acknowledgement are separate state transitions. An accepted tender creates an immutable commitment; it is not operationally handed over until the executing system acknowledges that it created or linked the corresponding local record. ## D018 — Trust is organizational, technical and bilateral An access token alone is not sufficient trust. The private exchange requires: - a verified organization identity; - a registered connector or interactive client bound to that organization; - an active bilateral partner relationship; - granted API scopes and relationship policy; - resource-specific visibility and disclosure policy; - actor authorization for the requested lifecycle transition. Machine clients use the protocol security profile. Interactive users authenticate through OpenID Connect. Internal TMS session tokens are not portable exchange credentials. ## D019 — The exchange is a coordination control plane The exchange is authoritative for partner relationships, exchanged resources, decisions, commitments, handover state and the shared audit journal. Connected systems remain authoritative for their local commercial, planning and execution records. Connectors translate between local models and canonical protocol resources, preserve references, process messages idempotently, quarantine unsafe mappings and reconcile after missed delivery or downtime. ## D020 — Profiles keep the canonical contract bounded The protocol is organized into composable profiles: 1. core identity, relationship, concurrency and error behavior; 2. discovery for capacity, demand and matching; 3. tendering and immutable commitment; 4. execution handover, amendment, cancellation and operational updates; 5. security and connector conformance. An implementation advertises the versions and optional capabilities it supports. Unsupported optional capabilities must fail explicitly rather than being silently ignored. --- # OpenFreytProtocol Context Source: CONTEXT.md **Status:** Authoritative project context, pre-alpha ## 1. Purpose OpenFreytProtocol enables logistics organizations that trust each other to discover capacity and demand, tender transport work, create explicit commitments and transfer execution between heterogeneous transport management systems. The initial product profile is a closed **private exchange**. It allows accepted partners to publish constrained supply and demand, discover compatible opportunities, negotiate versioned tenders, record authorized commitments, transfer a bounded execution package and share execution outcomes. Freyt is the first reference client, not the mandatory source of truth. Other TMS products, brokers, integration platforms and lightweight portals participate through the same public contract. ## 2. Problem boundary Transport organizations frequently have: - empty return movements; - partially used vehicles; - recurring lanes with unused capacity; - transport demand a trusted partner could execute; - fragmented partner-capacity information; - incompatible TMS data models; - no neutral, low-friction exchange contract. Public freight exchanges solve a different problem. They typically optimize broad market discovery and spot transactions. OpenFreytProtocol focuses on controlled collaboration between known organizations, with explicit authorization and limited information disclosure. ## 3. Protocol thesis A dependable partner exchange requires: 1. bilateral partner acceptance; 2. explicit data-sharing scopes; 3. minimal disclosure before commitment; 4. precise capacity and demand semantics; 5. deterministic, explainable compatibility; 6. explicit acceptance and withdrawal; 7. idempotent commands and ordered resource history; 8. implementation-independent conformance tests. The protocol standardizes the exchange boundary. It does not replace the planning, pricing, external legal contracting or local execution responsibilities of participating systems. The exchange is authoritative for shared relationships, tenders, commitments, handover state and audit history; each connected system remains authoritative for its local operational records. ## 4. Actors - **Capacity provider:** publishes available vehicle or trip capacity. - **Capacity requester:** publishes demand for transport capacity. - **Partner administrator:** establishes relationships, permissions and disclosure policy. - **Planner:** evaluates proposals and tenders, records decisions and reviews amendments or exceptions. - **Executing partner:** accepts committed work and acknowledges creation or linkage of its local execution record. - **Reference broker:** authorizes discovery, maintains exchange state, records commitments, coordinates handover and emits events. - **TMS connector:** maps a participant's internal model to the public protocol, preserves references, quarantines unsafe mappings and reconciles after downtime. - **Reference client:** Freyt is the first rich implementation without privileged protocol semantics. An organization may perform more than one role. ## 5. Initial use cases ### 5.1 Empty return capacity A provider publishes that a vehicle can carry a compatible load on or near a return corridor within a bounded time window. ### 5.2 Residual capacity A planned movement has available weight, volume, pallet-space or loading-metre capacity that can be offered to accepted partners. ### 5.3 Load request A partner publishes demand using coarse pickup and delivery areas, time windows, capacity requirements and operational constraints. ### 5.4 Recurring corridor Partners describe repeating supply or demand on known corridors without exposing their complete planning data. ### 5.5 Match decision The broker or an authorized participant proposes a compatible offer/request pair. The involved organizations explicitly accept, reject or allow the proposal to expire. ### 5.6 Tender and commitment A selected partner receives a versioned transport tender. Required authorized decisions over an immutable revision create a commitment. Counterproposals and material amendments preserve prior terms and require the decisions defined by bilateral policy. ### 5.7 Execution handover After commitment, the source transfers the minimum authorized stops, time windows, cargo facts, constraints, references and instructions needed to execute the movement. The destination acknowledges whether it imported, linked, rejected or quarantined the package. Both systems retain their local records. ## 6. Included scope - organization references; - bilateral partner relationships; - scoped visibility and permissions; - capacity offers and load requests; - geographic areas and time windows; - vehicle, equipment and capacity constraints; - explainable match proposals; - versioned transport tenders and counterproposals; - immutable commitments and accepted-payload digests; - bounded execution handovers and destination acknowledgements; - amendments, cancellation, exceptions and execution updates; - organization, connector and bilateral trust policy; - idempotent commands and optimistic concurrency; - lifecycle events and reconciliation; - immutable decision history; - execution references and outcome status; - public examples and conformance tests; - a reference broker and independent clients. ## 7. Explicit non-goals - public anonymous freight marketplace; - mandatory use of one TMS; - auctions or dynamic price discovery; - escrow, payments, clearing or claims; - automatic legal contract formation; - autonomous award without authorized confirmation; - complete order, shipment, vehicle or customer synchronization; - full route optimization; - multi-leg orchestration in the first profile; - universal logistics ontology; - mandatory dataspace or federation infrastructure; - dependency on proprietary, inaccessible or insufficiently mature protocols. Existing commercial agreements, prices, liability terms and execution contracts remain outside the core protocol. ## 8. Architecture direction ### 8.1 Neutral reference broker The first implementation uses a central broker for consistent authorization, matching, lifecycle management, idempotency and event publication. The broker is neutral at its public boundary: - no Freyt-internal types or database identifiers; - no privileged Freyt operation; - all participants use the published contract; - an independent broker implementation remains possible; - clients can be validated without access to Freyt source code. ### 8.2 Small canonical model The protocol defines only concepts required for partner capacity exchange: - `OrganizationReference`; - `PartnerRelationship`; - `CapacityOffer`; - `LoadRequest`; - `MatchProposal`; - `MatchDecision`; - `ExecutionReference`; - `ExecutionOutcome`. Complete transport orders, route plans, customer records and invoices remain external references or adapter concerns. ### 8.3 Normative layers - JSON Schema 2020-12 defines shared payload semantics. - OpenAPI 3.1 defines synchronous HTTP behaviour. - CloudEvents defines asynchronous event envelopes. - State-machine specifications define legal transitions. - Examples and conformance tests define observable compatibility. ### 8.4 Adapter boundary Existing enterprise or sector formats may be supported when a real participant requires them. Adapters map at the edge and cannot silently redefine core semantics. Every lossy mapping must be documented. ## 9. Trust and data sharing - Relationships require explicit acceptance. - Offers and requests are not globally visible by default. - Authorization is evaluated for every command, read and event delivery. - Visibility can be limited to named partners or relationship-defined groups. - Exact addresses, customer identities, pricing and sensitive goods details may remain hidden until the configured disclosure phase. - Participants control publication, withdrawal and future availability. - Historical decisions remain auditable after withdrawal or termination. - Logs contain correlation identifiers but avoid sensitive payloads. - Organization data must never leak through reads, errors, search, metrics, events or caches. ## 10. Matching principles The first matcher is deterministic and explainable. Compatibility can consider: - active partner authorization; - origin and destination area overlap; - pickup and delivery windows; - direction and acceptable detour; - vehicle and body type; - weight, volume, pallet and loading-metre capacity; - equipment and handling requirements; - goods restrictions; - participant policy constraints. Every proposal includes structured reason codes and the observed resource versions. Ambiguous or incompatible cases abstain. A proposed match does not disclose protected data or create a commitment. ## 11. Reliability principles - Commands are idempotent. - Concurrent updates use explicit resource versions. - Event delivery is at least once. - Consumers deduplicate events and reject state regression. - Accepted decisions are append-only. - Broker restarts cannot lose committed state. - Expired or withdrawn resources cannot be accepted. - Failure responses are stable, typed and correlated. - Every state transition is reconstructable from durable history. ## 12. Interoperability principles - Public contracts are implementation-neutral. - Primitive values use mature public standards. - Extensions are namespaced and cannot change core behaviour. - Breaking semantic changes require a major version. - Every supported version has immutable schemas. - Compatibility is proven through black-box conformance tests. - Stable releases require at least one independently implemented client. ## 13. Success criteria The protocol succeeds when: - two independent organizations can integrate without sharing a TMS; - a participant can publish capacity or demand with minimal disclosure; - only authorized partners can discover it; - a proposal can be explained and explicitly decided; - retries and concurrent actions cannot create duplicate or contradictory commitments; - accepted matches can be linked to each participant's execution records; - independent implementations pass the same conformance suite; - errors and recovery paths are operationally diagnosable. ## 14. Open questions - Which organization-identifier strategy is sufficiently open and practical? - What is the smallest useful vehicle/body taxonomy? - Which geographic area representations belong in the first profile? - Which party or foundation owns protocol governance and trademarks? - Which disclosure policies are required before and after acceptance? - Does commitment require one acceptance, bilateral acceptance or configurable policy? - How are cancellations and execution disputes represented? - Which first non-Freyt client proves independence? - Which adapter is justified by the first participating systems? - What retention periods apply to exchange and audit data? ## 15. Current direction Build a small, open and rigorously verified Partner Capacity Exchange protocol. Prioritize one complete interoperable exchange over broad semantic coverage. Expand only when concrete partner use cases and conformance evidence justify the change. --- # Quality Strategy Source: QUALITY-STRATEGY.md **Strategy:** Assurance-Driven Protocol Development **Status:** Required foundation before production implementation **Applies to:** protocol specifications, schemas, conformance suites, reference broker, clients, adapters and release artifacts ## 1. Quality objective Absolute certainty is not an honest engineering claim. OpenFreytProtocol instead requires **100% traceability of every critical claim and invariant to executable evidence**. Every critical capability follows this chain: ```text Risk → Requirement → Normative specification clause → Invariant or acceptance criterion → Automated verification → Durable release evidence ``` A feature is not considered safe because code coverage is high or a happy-path test passes. A release is justified only when its relevant safety, security, compatibility and recovery claims have current evidence. ## 2. Non-negotiable principles 1. Specify observable behaviour before implementation. 2. Use test-driven development for domain and application behaviour. 3. Keep domain rules independent from frameworks and infrastructure. 4. Treat the public specification and conformance suite as the product. 5. Formally model the small set of distributed state transitions that can produce invalid commitments or data leakage. 6. Verify every critical invariant with multiple complementary techniques. 7. Test real persistence and network boundaries; do not mock away operational risk. 8. Require an independent implementation for interoperability proof. 9. Preserve immutable verification and release evidence. 10. Never waive a failing or flaky test silently. ## 3. Uncle Bob practices ### 3.1 Test-driven development Use the red-green-refactor cycle: 1. Write the smallest failing test demonstrating the missing behaviour. 2. Write only enough production code to pass. 3. Refactor while the suite remains green. 4. Start every defect fix with a reproducing test. Tests describe externally meaningful behaviour. They must not be coupled to private methods, framework internals or incidental call order. ### 3.2 Clean Architecture Implementation dependencies point inward: ```text Domain Pure state, matching, authorization and evidence rules Application Commands, policies and use cases Ports Persistence, event publication, identity, clock and routing abstractions Adapters HTTP, database, queue, TMS and external-format integrations ``` The domain must not depend on HTTP frameworks, databases, cloud SDKs, Freyt internals or adapter-specific models. ### 3.3 Humble adapters Adapters validate, translate and delegate. Business decisions remain in the domain or application core. Prefer in-memory implementations for application tests and use mocks only at actual process or network boundaries. TDD and Clean Architecture are necessary but not sufficient. Distributed retries, concurrency, authorization and compatibility also require formal, model-based, integration, security and resilience verification. ## 4. Executable specification first Before implementing a production endpoint, define: - stable requirement IDs; - RFC 2119/RFC 8174 `MUST`, `SHOULD` and `MAY` clauses; - glossary and canonical field meanings; - JSON Schemas; - state-transition tables; - command preconditions and outcomes; - authorization matrix; - idempotency and concurrency behaviour; - event ordering and delivery guarantees; - compatibility rules; - valid and invalid examples; - stable error codes. The reference implementation is not normative. Normative behaviour comes from the specification, schemas and conformance suite. Schemas, examples and conformance checks must run in CI before the first broker implementation exists. ## 5. Requirements and traceability Maintain machine-readable registries: ```text requirements/requirements.yaml requirements/invariants.yaml requirements/traceability.yaml ``` Every requirement contains: - stable ID; - risk or user need; - normative statement; - owner; - criticality; - affected resources and operations; - acceptance criteria; - test/evidence references; - lifecycle status. CI fails when: - a critical requirement has no test reference; - a normative schema or operation has no requirement; - a removed test leaves an active requirement unverified; - a release contains stale evidence. Target **100% requirement, invariant, lifecycle-transition and authorization-matrix coverage**. ## 6. Formal model of critical state Use a focused TLA+/PlusCal model for: - offer and request lifecycle; - proposal creation; - concurrent accept, reject, withdrawal and expiry; - duplicate commands; - stale resource versions; - duplicate and out-of-order events; - restart/recovery boundaries; - execution-evidence deduplication. Required safety properties include: ```text No commitment without every decision required by policy. No commitment after a valid withdrawal or expiry. One idempotency key cannot produce two different effects. An unauthorized organization never observes a protected resource. Duplicate or reordered events cannot regress resource state. One accepted exchange cannot produce contradictory execution outcomes. Corrections cannot erase historical decisions or evidence. ``` Formal modeling is deliberately restricted to distributed lifecycle invariants. It is not required for simple CRUD or presentation behaviour. ## 7. Independent reference model Build a pure deterministic reference model independently from the broker. It accepts commands such as: ```text RequestRelationship ActivateRelationship PublishOffer PublishLoadRequest ProposeMatch AcceptMatch RejectMatch WithdrawOffer ExpireProposal RecordExecution RecordEvidence CorrectEvidence ``` Model-based tests generate command sequences and compare: - expected model state; - broker state; - emitted events; - visible projections; - audit history. Randomized failures always report and preserve their seed. ## 8. Verification layers ### 8.1 Static verification Run on every change: - strict compilation; - formatter and linter; - JSON Schema validation; - OpenAPI and AsyncAPI linting; - broken-reference detection; - generated-artifact drift detection; - breaking-change detection; - dependency and license policy; - secret scanning; - static security analysis. ### 8.2 Domain unit tests Fast, pure tests cover: - lifecycle transitions; - authorization decisions; - capacity compatibility; - matching reason codes; - distance and evidence calculations; - decision policy; - correction and supersession. These tests do not start a database, queue or web server. ### 8.3 Property-based tests Generate and shrink cases involving: - timestamp and time-zone boundaries; - malformed and extreme capacity values; - invalid or boundary geometry; - corridor overlap; - duplicate identifiers; - stale versions; - arbitrary valid and invalid transition sequences; - unit conversion and numerical rounding; - Unicode and hostile external-reference values. ### 8.4 Mutation tests Mutation testing measures whether assertions detect broken logic. Critical modules require no unexplained surviving mutation. Examples include removing an authorization check, changing `>` to `>=`, skipping a version check or reversing a terminal transition. Equivalent mutations may be waived only with a reviewed explanation stored as evidence. ### 8.5 Schema and contract tests For every resource and operation, maintain: - valid golden examples; - invalid examples for each validation rule; - request and response conformance; - event conformance; - unknown-field behaviour; - enum compatibility behaviour; - stable RFC 9457 error responses. The suite must run against any implementation without importing Freyt code. ### 8.6 Persistence integration tests Use the real database technology to verify: - unique and foreign-key constraints; - transaction boundaries; - optimistic concurrency; - idempotency records; - append-only decisions and evidence; - transactional outbox behaviour; - organization isolation; - migration and restart recovery. Do not replace persistence behaviour with mocks. ### 8.7 End-to-end tests Exercise only public interfaces. Cover complete success and rejection paths: ```text relationship → offer/request → proposal → decisions → execution → evidence → audit retrieval ``` Include withdrawal, expiry, stale decisions, cancellation and evidence correction. ### 8.8 Independent interoperability A stable release requires: - one reference broker; - the Freyt reference client; - one independently implemented client or simulator; - preferably an implementation in another programming language. Every implementation runs the same black-box conformance suite. Shared implementation libraries must not hide interoperability defects. ### 8.9 Resilience and recovery Inject failures at every distributed boundary: - database unavailable; - event transport unavailable; - timeout after commit but before response; - duplicate delivery; - out-of-order delivery; - expired credentials; - unavailable webhook; - restart during acceptance; - adapter partial failure; - clock skew. Verify state, retry safety, operator diagnostics and eventual recovery. ### 8.10 Security and privacy Maintain a threat model and test: - organization and object-level isolation; - field-level disclosure; - token audience and scopes; - replay resistance; - webhook authentication; - resource enumeration resistance; - injection and hostile references; - oversized payload handling; - sensitive log redaction; - retention and deletion; - software-supply-chain risk. Use the OWASP API Security Top 10 as a required release checklist. No unresolved critical or high finding is releasable without an explicit, time-bounded risk acceptance. ### 8.11 Performance and longevity Define service-level targets before running tests: - expected and 10× resource/event volume; - API latency percentiles; - matching deadline; - replay and recovery time; - evidence-report time; - retention volume; - backpressure behaviour. Use sustained load and soak tests in addition to short peak tests. ## 9. Coverage policy Line coverage is a diagnostic, not the quality goal. Required coverage: - 100% active requirement coverage; - 100% documented invariant coverage; - 100% state-transition coverage; - 100% authorization-matrix coverage; - 100% branch coverage for critical domain modules; - no unreviewed mutation survivors in critical modules; - zero skipped, focused or quarantined tests; - zero accepted flaky tests. Critical modules include partner authorization, visibility, state transitions, idempotency, commitment creation, event ordering and execution-outcome deduplication. ## 10. Continuous-integration gates ### Every pull request - specification and schema validation; - canonical-content policy and generated documentation drift checks; - static documentation-site type check and production build; - strict type checking; - linting and formatting; - unit and property tests; - schema and contract tests; - database integration tests; - compatibility diff; - security/static analysis; - affected-code mutation testing; - traceability validation; - no skipped tests; - independent review. ### Nightly - full mutation suite; - long-running property-based tests; - fuzzing; - concurrency stress; - restart and fault-injection suite; - complete dependency and license scan; - performance regression tests. ### Release candidate A release is blocked unless: - every critical requirement has current evidence; - all applicable conformance profiles pass; - two independent clients interoperate; - all supported prior client versions pass compatibility tests; - migrations pass from every supported version; - rollback or forward recovery has been rehearsed; - threat model and authorization matrix are current; - no unresolved critical/high security finding remains; - performance and recovery targets pass; - schemas, release artifacts and SBOM are signed; - the versioned website is generated from the exact release revision; - release evidence is archived. ## 11. Repository quality structure ```text / ├── README.md ├── CONTEXT.md ├── QUALITY-STRATEGY.md ├── CONTRIBUTING.md ├── astro.config.mjs ├── scripts/ │ ├── check-content.mjs │ └── sync-docs.mjs ├── src/content/docs/ Generated website content plus landing page ├── requirements/ │ ├── requirements.yaml │ ├── invariants.yaml │ └── traceability.yaml ├── specification/ │ ├── protocol.md │ ├── glossary.md │ ├── state-machines/ │ ├── schemas/ │ ├── openapi/ │ ├── events/ │ └── errors/ ├── examples/ │ ├── valid/ │ ├── invalid/ │ └── scenarios/ ├── conformance/ │ ├── reference-model/ │ ├── provider-suite/ │ ├── consumer-suite/ │ └── evidence-suite/ ├── formal/ │ └── tla/ ├── implementations/ │ ├── reference-broker/ │ ├── freyt-client/ │ └── independent-client/ ├── adapters/ ├── security/ │ ├── threat-model.md │ ├── authorization-matrix.md │ └── abuse-cases.md ├── verification/ │ ├── performance/ │ ├── resilience/ │ └── releases/ └── docs/ ├── adr/ ├── rfcs/ └── research/ ``` Because the root `DECISIONS.md` is locally ignored, durable normative architecture decisions must be stored as tracked ADRs under `docs/adr/` before implementation begins. ## 12. Definition of Done A capability is done only when: - its requirement and risk are identified; - normative semantics are documented; - security and privacy impact are assessed; - a failing acceptance or domain test preceded implementation; - positive, negative and boundary tests pass; - property/model tests cover relevant behaviour; - integration and conformance tests pass; - failure and recovery behaviour is verified; - observability and redaction are verified; - compatibility impact is recorded; - traceability evidence is current; - an independent reviewer approves it. ## 13. Foundation gate before production code No production broker or SDK implementation should begin until these artifacts exist and pass validation: 1. quality charter and contribution policy; 2. requirement and invariant registries; 3. glossary; 4. state-transition specifications; 5. authorization matrix; 6. initial threat model and abuse cases; 7. conformance-test skeleton; 8. CI checks for schema, traceability and breaking changes; 9. release-evidence format; 10. tracked ADRs for all normative architecture choices. The first implementation milestone is therefore not an API endpoint. It is an executable assurance foundation. --- # Invariants Source: requirements/invariants.yaml # Invariants Registry # OpenFreytProtocol — machine-readable safety and security invariants # # Fields per entry: # id — stable identifier, format INV-NNN # title — short human name # requirement — linked requirement ID(s) # statement — the invariant in plain terms (what must always be true) # type — safety | security | liveness | consistency # formal — true if this is modeled in formal/tla/ # tests — list of test IDs that verify this invariant # status — active | deprecated invariants: - id: INV-001 title: No commitment without all required decisions requirement: [R-002] statement: > For any proposal P, a commitment record for P cannot exist unless every decision required by the bilateral policy of P's relationship has been recorded as accepted. type: safety formal: true tests: [] status: active - id: INV-002 title: No commitment after valid withdrawal or expiry requirement: [R-002] statement: > If a withdrawal or expiry event for offer O or proposal P has been accepted, no subsequent command can produce a commitment record referencing O or P. type: safety formal: true tests: [] status: active - id: INV-003 title: Idempotency — one key, one effect requirement: [R-002, R-003] statement: > A command bearing idempotency key K produces the same effect on every replay. Duplicate delivery of K cannot produce two different effects or two different commitment records. type: consistency formal: true tests: [] status: active - id: INV-004 title: Unauthorized organization cannot observe protected resource requirement: [R-001] statement: > For any organization O and resource R, if O is not an authorized participant in the relationship owning R, then O receives no data from R in any API response, event, or audit record. type: security formal: false tests: [] status: active - id: INV-005 title: Duplicate or reordered events cannot regress resource state requirement: [R-003] statement: > Receiving the same event twice, or receiving events out of causal order, must not move a resource to an earlier lifecycle state or erase an accepted decision. type: consistency formal: true tests: [] status: active - id: INV-006 title: Evidence deduplication — one execution, one contribution requirement: [R-003] statement: > A single execution record cannot contribute to avoided-kilometre totals or evidence aggregations more than once, regardless of how many times the evidence record is submitted. type: consistency formal: true tests: [] status: active - id: INV-007 title: Corrections are append-only requirement: [R-003] statement: > A correction command produces a new superseding record that references the prior record. The prior record is never mutated or deleted. type: safety formal: false tests: [] status: active - id: INV-008 title: One handover creates at most one destination execution requirement: [R-007, R-009] statement: > Replaying or reordering delivery of one execution handover cannot create a second destination execution record or a conflicting acknowledgement. type: consistency formal: true tests: [] status: active - id: INV-009 title: Accepted terms cannot change silently requirement: [R-010] statement: > No amendment or counterproposal can replace the decision-relevant payload of an accepted tender revision without preserving the accepted snapshot and recording every renewed decision required by bilateral policy. type: safety formal: true tests: [] status: active - id: INV-010 title: Authenticated organization binding cannot be overridden requirement: [R-001, R-008] statement: > For every connector command, the effective organization is the organization bound to the authenticated connector registration. No payload, external reference, tenant field, or callback can change that effective organization. type: security formal: false tests: [] status: active - id: INV-011 title: Commitment and handover remain distinct requirement: [R-007] statement: > Creating a commitment cannot by itself place the corresponding handover in an imported or linked state. Only an authorized destination acknowledgement can establish technical handover success. type: safety formal: true tests: [] status: active --- # Requirements Source: requirements/requirements.yaml # Requirements Registry # OpenFreytProtocol — machine-readable requirement definitions # # Fields per entry: # id — stable identifier, format R-NNN # title — short human name # risk — the risk or user need this addresses # statement — normative statement (RFC 2119 language) # owner — responsible party # criticality — critical | high | medium | low # resources — affected protocol resources / operations # acceptance — list of measurable acceptance criteria # tests — list of test file paths or test IDs (populated as tests are written) # status — active | deprecated | superseded # adr — linked ADR(s) # # CI fails when: # - a critical requirement has no entry in traceability.yaml # - a normative schema or operation has no requirement # - a removed test leaves an active requirement unverified requirements: - id: R-001 title: Closed partner network risk: Unrestricted data exposure between non-partner organizations statement: > An organization MUST NOT observe capacity offers, load requests, match proposals, or execution records belonging to a relationship in which it is not an authorized participant. owner: protocol criticality: critical resources: [relationships, offers, requests, proposals, executions] acceptance: - A non-partner organization receives HTTP 403 or equivalent for every protected resource. - Organization isolation is verified by integration test with two independent organizations. - Authorization matrix is complete and test-referenced. tests: [] status: active adr: [D001, D012] - id: R-002 title: Explicit commitment only risk: Unintended operational commitment formed without authorized acceptance statement: > A match proposal MUST NOT become an operational commitment without explicit acceptance by all parties required by the bilateral decision policy. The protocol MUST NOT automatically form legal contracts, settle payments, or award work without confirmation. owner: protocol criticality: critical resources: [proposals, decisions] acceptance: - No commitment record exists unless an explicit accept command was issued. - Concurrent accept and reject or withdrawal resolves without silent commitment. - Formal model property verified: no commitment without every required decision. tests: [] status: active adr: [D009, D012] - id: R-003 title: Immutable lifecycle history risk: Silent rewriting of accepted decisions or evidence corrupts audit trail statement: > Accepted decisions, execution records, and evidence artifacts MUST NOT be silently overwritten. Corrections MUST append or supersede records and MUST retain the reason, actor, timestamp, and prior relationship. owner: protocol criticality: critical resources: [decisions, executions, evidence] acceptance: - Correction commands produce a new record referencing the prior record. - No PUT or PATCH operation overwrites an accepted decision. - Append-only constraint verified by persistence integration test. tests: [] status: active adr: [D010, D012] - id: R-004 title: Implementation-independent conformance risk: Proprietary implementation becomes the de facto standard; interoperability unverified statement: > A stable release MUST demonstrate a reference broker, the Freyt reference client, and at least one independently implemented client or simulator all passing the same black-box conformance suite. owner: protocol criticality: critical resources: [conformance-suite] acceptance: - Conformance suite runs without importing reference-broker or Freyt-client code. - Two independent clients demonstrate interoperability on the same suite. tests: [] status: active adr: [D011, D012] - id: R-005 title: Minimum disclosure before match phase risk: Commercially sensitive data disclosed before partner consent or match phase statement: > Before the configured match phase, the protocol MUST share only the corridor, time window, vehicle type, capacity, and compatibility information needed for discovery. Customer names, exact addresses, prices, and commercially sensitive execution details MUST NOT be exposed in pre-match API responses. owner: protocol criticality: critical resources: [offers, requests, proposals] acceptance: - Pre-match API responses contain no customer name, exact address, or price fields. - Field-level disclosure is verified by contract test on schema examples. tests: [] status: active adr: [D008, D012] - id: R-006 title: Idempotent commands risk: Duplicate network delivery causes unintended side-effects or double-commits statement: > Every mutating operation MUST accept an Idempotency-Key header scoped to the authenticated organization and the operation type. A repeated request carrying the same key MUST produce the same observable effect and response code as the first accepted request. owner: protocol criticality: critical resources: [offers, requests, proposals, decisions, executions, evidence] acceptance: - Repeated POST with identical Idempotency-Key returns the original response body and 2xx status. - Key collision across different organizations or operation types is rejected with 422. - Idempotency behaviour is verified by contract test replaying each mutating operation. tests: [] status: active adr: [D012] - id: R-007 title: Optimistic concurrency control risk: Lost update due to concurrent mutations producing inconsistent resource state statement: > All protocol resources MUST carry a monotonically incrementing resourceVersion integer. Any command that references a stale resourceVersion MUST be rejected with HTTP 409 Conflict. owner: protocol criticality: high resources: [offers, requests, proposals, decisions, executions, relationships] acceptance: - Command referencing current resourceVersion succeeds and increments the version. - Command referencing a prior resourceVersion returns HTTP 409 with problem+json body. - Concurrent update race verified by integration test with two simultaneous mutations. tests: [] status: active adr: [D012] - id: R-008 title: Versioned schemas with additive-only evolution risk: Breaking schema change silently invalidates existing producers or consumers statement: > Changes within a major API version MUST be additive only (new optional fields, new enum values with documented default handling). Breaking changes MUST increment the major version and MUST be accompanied by a machine-readable compatibility diff. owner: protocol criticality: high resources: [conformance-suite, schemas] acceptance: - Schema diff tooling detects removal or type change of any existing field and fails CI. - A minor-version bump adding an optional field passes the compatibility diff check. - Breaking change without major-version increment is rejected by CI gate. tests: [] status: active adr: [D012] - id: R-009 title: Deterministic compatibility matching risk: Opaque ML scoring produces unauditable match decisions that cannot be reproduced statement: > Match proposals MUST record which compatibility checks passed, failed, or abstained for each proposal. No opaque scoring model MAY be the sole basis for accepting or rejecting a match. A matcher MUST abstain — not fail — when required compatibility data is missing. owner: protocol criticality: high resources: [proposals, requests, offers] acceptance: - Every match proposal response includes a compatibility-checks array with pass/fail/abstain per check. - A proposal generated with missing vehicle-type data shows abstain, not fail, for that check. - Deterministic re-run of the same inputs produces an identical compatibility-checks result. tests: [] status: active adr: [D012] - id: R-010 title: RFC 9457 structured error responses risk: Opaque or inconsistent error bodies prevent automated error handling and interoperability statement: > All error responses MUST use Content-Type application/problem+json and MUST include the fields type, title, status, detail, and instance as defined by RFC 9457. A protocol-maintained error catalogue MUST enumerate all protocol-specific problem types. owner: protocol criticality: high resources: [offers, requests, proposals, decisions, executions, relationships, evidence] acceptance: - Every 4xx and 5xx response carries Content-Type application/problem+json. - Response body includes type, title, status, detail, and instance fields. - Error catalogue file lists every protocol-specific type URI with description. - Contract test validates all documented error responses against RFC 9457 schema. tests: [] status: active adr: [D012] - id: R-011 title: Open governance and permissive licensing risk: Vendor lock-in if protocol artifacts are proprietary; community cannot fork or validate statement: > Schemas, canonical examples, and conformance tooling MUST be published under a permissive open-source license (MIT, Apache-2.0, or equivalent). All normative protocol changes MUST be tracked as Architecture Decision Records (ADRs). owner: protocol criticality: medium resources: [conformance-suite, schemas] acceptance: - LICENSE file at repository root identifies an OSI-approved permissive license. - All normative changes since v0.1 have a corresponding ADR under docs/adr/. - CI verifies that schema and conformance directories carry the declared license header. tests: [] status: active adr: [D012] - id: R-012 title: Formal lifecycle safety model risk: Undiscovered invariant violation allows commitment without consent or audit loss statement: > Safety invariants INV-001 through INV-006 MUST be formally modelled in TLA+/PlusCal and verified by TLC with zero violations. The model MUST cover all state transitions in the offer, request, proposal, and commitment lifecycle. owner: protocol criticality: critical resources: [proposals, decisions, executions] acceptance: - TLC model checker runs against formal/tla/LifecycleSafety.tla with zero violations. - Model covers all six invariants INV-001 through INV-006. - CI includes a TLC verification step that fails on any TLC error output. tests: [] status: active adr: [D012] - id: R-013 title: No Freyt-specific identifiers risk: Protocol dependency on proprietary registry creates single-point lock-in statement: > No protocol operation SHALL require a Freyt account, Freyt-issued identifier, or Freyt-owned registry. All identifiers MUST be broker-scoped (organizationId) or use openly registered namespaces (GLN, EORI, DUNS, or equivalent). owner: protocol criticality: high resources: [relationships, offers, requests, proposals, decisions, executions] acceptance: - Specification contains no reference to a Freyt-owned identifier registry. - A conformance test demonstrates broker-scoped organizationId accepted without Freyt lookup. - Schema validation rejects any field typed as freyt-id or similar proprietary namespace. tests: [] status: active adr: [D012] - id: R-014 title: Threat model before broker implementation risk: Security vulnerabilities introduced during implementation without prior threat analysis statement: > The files threat-model.md and abuse-cases.md MUST exist and be reviewed before any broker implementation pull request is merged. No unresolved critical or high security finding MAY exist without an explicit, recorded risk-acceptance decision. owner: protocol criticality: critical resources: [relationships, offers, requests, proposals, decisions, executions, evidence] acceptance: - security/threat-model.md exists with at least one threat entry per protocol resource. - security/abuse-cases.md exists with at least one abuse case per authentication boundary. - CI gate verifies both files exist before any implementation PR target branch is merged. - All critical and high findings are either resolved or carry a signed risk-acceptance record. tests: [] status: active adr: [D012] - id: R-015 title: Complete authorization matrix risk: Undefined access control leaves resources unprotected or incorrectly restricted statement: > The authorization matrix MUST cover all 9 OAuth scopes, 7 protocol resources, and 4 roles before any broker implementation pull request is merged. The matrix MUST explicitly enumerate allow, deny, and condition for every scope-resource-role combination. owner: protocol criticality: critical resources: [relationships, offers, requests, proposals, decisions, executions, evidence] acceptance: - security/authorization-matrix.md contains a complete 9x7x4 entry table. - Every cell specifies allow, deny, or a named condition — no blank or TBD cells. - CI gate verifies matrix completeness before any implementation PR is merged. - Contract test validates at least one allow and one deny path per resource per role. tests: [] status: active adr: [D012] # ── Requirements R-016 through R-021 (merged from main) ─────────────────── - id: R-016 title: Automatically generated static protocol website risk: Published documentation drifts from the reviewed protocol sources and misleads implementers statement: > Every development and production documentation build MUST validate canonical repository content and MUST regenerate the static website, protocol reference, downloadable artifacts, and machine-readable documentation indexes from the same source revision. Generated pages MUST NOT be maintained as independent sources. owner: documentation criticality: high resources: [documentation-site, schemas, examples, decisions, conformance-suite] acceptance: - The build fails when canonical content violates repository content policy. - The build regenerates pages from canonical Markdown and structured artifacts. - The production site contains protocol, quality, decision, and reference pages. - The production site publishes llms.txt and llms-full.txt. - Generated content and build output are not committed as canonical source. tests: - scripts/check-content.mjs - scripts/sync-docs.mjs - .github/workflows/docs.yml status: active adr: [D012, D013, D014] - id: R-017 title: Bounded execution handover risk: Connected systems cannot reliably materialize an accepted transport assignment statement: > After a valid commitment, the protocol MUST support a vendor-neutral execution handover containing the minimum authorized operational data needed by the executing organization. The executing connector MUST acknowledge imported, linked, rejected, or quarantined without creating duplicate local work on replay. owner: protocol criticality: critical resources: [tenders, commitments, handovers, acknowledgements, external-references] acceptance: - A conforming receiver creates or links exactly one local record for duplicate handover delivery. - Commercial acceptance and technical import acknowledgement have separate observable states. - Source and destination external references remain namespaced and traceable. - Unsafe or incomplete mappings are rejected or quarantined without invented required values. tests: [] status: active adr: [D015, D016, D017] - id: R-018 title: Organization and connector trust risk: A valid technical credential is mistaken for authorization to exchange partner data statement: > Every protected operation MUST validate the active organization, registered connector or user, client scope, bilateral relationship policy, resource visibility, disclosure phase, actor permission, lifecycle preconditions, and current resource version. Payload fields MUST NOT override the authenticated organization binding. owner: protocol criticality: critical resources: [organizations, connectors, relationships, authorization] acceptance: - A connector credential is bound to exactly one organization and environment. - Passing an API scope without bilateral permission cannot reveal or mutate a protected resource. - A mismatched payload owner is rejected and cannot redirect processing to another organization. - Credential and relationship suspension behavior is covered by integration tests. tests: [] status: active adr: [D018] - id: R-019 title: Reliable multi-system synchronization risk: Duplicate, reordered, delayed, or missed delivery corrupts local or exchange state statement: > Connectors MUST process commands and events idempotently, retain durable mapping state, and reconcile authoritative resources after delivery failure or downtime. An accepted commitment whose handover import failed MUST remain observable as committed but not handed over. owner: protocol criticality: critical resources: [connectors, events, handovers, reconciliation] acceptance: - Duplicate and reordered events cannot regress lifecycle state. - Reconciliation recovers a missed handover and produces the same local mapping as direct delivery. - Import failures expose a stable error or quarantine state and are not reported as successful transfer. - Connector health, delivery lag, dead letters, and mapping failures are observable. tests: [] status: active adr: [D017, D019] - id: R-020 title: Immutable tender and amendment semantics risk: A party is bound to transport terms it did not review or accept statement: > A tender decision MUST reference an immutable tender revision. Any decision-relevant change after commitment MUST preserve the accepted snapshot and require renewed acceptance according to bilateral policy. Cancellation MUST be explicit and auditable. owner: protocol criticality: critical resources: [tenders, counterproposals, commitments, amendments, cancellations] acceptance: - Counterproposals and amendments create linked revisions rather than overwrite accepted terms. - A digest identifies the exact decision-relevant payload accepted by each required actor. - A material amendment cannot become effective without required renewed acceptance. - Deletion cannot substitute for cancellation of an active commitment. tests: [] status: active adr: [D015, D017] - id: R-021 title: Profile and capability negotiation risk: A connector silently drops mandatory data or behavior it does not support statement: > A connector MUST advertise its supported protocol profiles, versions and optional capabilities. Unsupported mandatory capabilities MUST fail with an explicit stable problem code and MUST NOT be silently discarded. owner: protocol criticality: high resources: [connectors, capabilities, conformance-suite] acceptance: - Capability documents are schema-valid and include profile versions. - Unsupported mandatory cargo, document, or lifecycle behavior fails explicitly. - Conformance tests exercise negotiation between independently implemented clients. tests: [] status: active adr: [D020] --- # Traceability Source: requirements/traceability.yaml # Traceability Registry # OpenFreytProtocol — links requirements and invariants to tests and evidence # # CI fails when: # - a critical requirement from requirements.yaml has no entry here # - an entry references a test file that does not exist # - an invariant marked formal:true has no tla_model entry # # Fields per entry: # requirement — R-NNN from requirements.yaml (or INV-NNN for invariant entries) # criticality — mirrors requirements.yaml criticality (used by CI check) # evidence: # unit_tests — list of test file paths # property_tests — list of test file paths # integration_tests — list of test file paths # conformance_tests — list of test file paths # tla_model — path to TLA+ spec (required when invariants.yaml formal: true) # status — pending | partial | covered # notes — free text traceability: # ── Requirements R-001 through R-015 ────────────────────────────────────── - requirement: R-001 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by security/authorization-matrix.md (9 scopes × 7 resources × 4 roles) and conformance isolation tests that verify cross-organization access returns HTTP 403. Both artifacts are planned for the assurance-foundation milestone. - requirement: R-002 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: > Formal model formal/tla/LifecycleSafety.tla verifies the no-commitment-without-decision invariant (INV-001, INV-002). Integration tests for concurrent accept/reject/withdraw race resolution and commitment invariant tests are planned for the assurance-foundation milestone. - requirement: R-003 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by formal/tla/LifecycleSafety.tla (INV-005, INV-006, INV-007) and append-only persistence integration tests that verify correction commands produce superseding records referencing prior records. No PUT/PATCH contract test planned. - requirement: R-004 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by conformance/README.md and the black-box conformance suite skeleton. Suite must run without importing reference-broker or Freyt-client code. Interoperability demonstration deferred to stable-release milestone. - requirement: R-005 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by specification/schemas/ and field-level disclosure contract tests that assert pre-match API responses contain no customer name, exact address, or price fields. Schema examples to be verified against disclosure rules. - requirement: R-006 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: > Coverage provided by formal/tla/LifecycleSafety.tla (INV-003: idempotency invariant) and contract tests replaying each mutating operation with an identical Idempotency-Key. Tests will verify same response body and 2xx, plus 422 on key collision across different organizations or operation types. - requirement: R-007 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by integration tests verifying that a command referencing a prior resourceVersion returns HTTP 409 Conflict with application/problem+json body, and that a successful command increments the version. Concurrent update race test planned. - requirement: R-008 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by specification/schemas/ and schema diff tooling in CI. Compatibility gate must detect removal or type change of any existing field. A minor-version bump adding an optional field must pass the diff check. - requirement: R-009 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by contract tests on match proposal responses that verify the compatibility-checks array includes pass/fail/abstain per check. A deterministic re-run property test verifies identical inputs produce identical results. Missing vehicle-type abstain case verified by unit test. - requirement: R-010 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by specification/errors/ error catalogue (all protocol-specific problem types) and contract tests validating every 4xx/5xx response carries Content-Type application/problem+json with type, title, status, detail, and instance fields as defined by RFC 9457. - requirement: R-011 criticality: medium evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by LICENSE file at repository root and docs/adr/ directory. CI gate verifies schema and conformance directories carry the declared license header. All normative changes tracked as ADRs; current decisions referenced via D001-D012. - requirement: R-012 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: > Coverage provided directly by formal/tla/LifecycleSafety.tla verified by TLC with zero violations. Model covers all six invariants INV-001 through INV-006 and all state transitions in offer, request, proposal, and commitment lifecycle. CI TLC verification step planned for assurance-foundation milestone. - requirement: R-013 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by specification review confirming no Freyt-owned identifier registry reference, conformance test demonstrating broker-scoped organizationId accepted without Freyt lookup, and schema validation rejecting proprietary namespace field types. - requirement: R-014 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by security/threat-model.md (at least one threat entry per protocol resource) and security/abuse-cases.md (at least one abuse case per authentication boundary). CI gate verifies both files exist before any implementation PR is merged. Both artifacts planned for assurance-foundation milestone. - requirement: R-015 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Coverage provided by security/authorization-matrix.md covering all 9 OAuth scopes, 7 protocol resources, and 4 roles with explicit allow/deny/condition per cell. CI gate verifies matrix completeness. Contract test validates at least one allow and one deny path per resource per role. # ── Invariants INV-001 through INV-007 ──────────────────────────────────── - requirement: INV-001 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: - conformance/src/profiles/consumer/commitment-safety.suite.ts - conformance/src/profiles/consumer/race-conditions.suite.ts tla_model: formal/tla/LifecycleSafety.tla status: verified notes: > Formal property: no commitment record can exist unless every required decision has been recorded as accepted. Verified by TLC against formal/tla/LifecycleSafety.tla and by executable checkInv001 predicate assertions in the Consumer conformance suite. - requirement: INV-002 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: - conformance/src/profiles/consumer/commitment-safety.suite.ts - conformance/src/profiles/consumer/race-conditions.suite.ts tla_model: formal/tla/LifecycleSafety.tla status: verified notes: > Formal property: no command can produce a commitment referencing a withdrawn or expired offer or proposal. Verified by TLC against formal/tla/LifecycleSafety.tla and by executable checkInv002 predicate assertions in the Consumer conformance suite. - requirement: INV-003 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: verified notes: > Formal property: a command bearing idempotency key K produces the same effect on every replay. Verified by TLC against formal/tla/LifecycleSafety.tla. - requirement: INV-004 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: verified notes: > Security invariant: unauthorized organization receives no data from protected resources. Formally modeled as OrganizationIsolation in formal/tla/LifecycleSafety.tla (verified by TLC, 0 violations, 10,296 distinct states). Full coverage also requires authorization-matrix conformance tests and integration isolation tests. - requirement: INV-005 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: verified notes: > Formal property: duplicate or reordered events cannot regress resource state. Verified by TLC against formal/tla/LifecycleSafety.tla. - requirement: INV-006 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: verified notes: > Formal property: a single execution record cannot contribute to evidence aggregations more than once. Verified by TLC against formal/tla/LifecycleSafety.tla. - requirement: INV-007 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: > Safety invariant: corrections are append-only; the prior record is never mutated. Not formally modeled (formal: false); verified by append-only persistence integration tests and correction-command contract tests. # ── Requirements R-016 through R-021 (merged from main) ─────────────────── - requirement: R-016 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: - scripts/check-content.mjs - scripts/sync-docs.mjs conformance_tests: - .github/workflows/docs.yml tla_model: ~ status: verified notes: pnpm check validates content, synchronizes canonical sources, type-checks the site, and builds the static artifact. - requirement: R-017 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: Handover schema, destination deduplication, acknowledgement, and quarantine tests are pending. - requirement: R-018 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: Organization binding and layered authorization matrix tests are pending. - requirement: R-019 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: Duplicate, reorder, outage, dead-letter, and reconciliation tests are pending. - requirement: R-020 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: Tender revision, renewed acceptance, cancellation race, and accepted-snapshot tests are pending. - requirement: R-021 criticality: high evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: Capability document schema and independent-client negotiation tests are pending. # ── Invariants INV-008 through INV-011 (merged from main) ───────────────── - requirement: INV-008 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: Formal and integration proof of one destination execution per handover is pending. - requirement: INV-009 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: Formal proof that accepted decision terms cannot change silently is pending. - requirement: INV-010 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: ~ status: pending notes: Cross-organization payload override and callback isolation tests are pending. - requirement: INV-011 criticality: critical evidence: unit_tests: [] property_tests: [] integration_tests: [] conformance_tests: [] tla_model: formal/tla/LifecycleSafety.tla status: pending notes: Formal proof that commitment cannot imply destination import is pending. --- # OpenFreyt Protocol — Abuse Case Catalogue Source: security/abuse-cases.md **Status:** Normative **Version:** 1.0.0-draft **Locked by:** M001-S01-T03 **Last reviewed:** 2026-08-31 --- ## Overview This catalogue documents adversarial abuse cases for the OpenFreyt Protocol (PCX). Each entry describes a concrete attack scenario from an attacker's perspective, the protocol controls that prevent or limit it, and the residual risk. Abuse cases complement the threat model. Where the threat model organises findings by threat category, this catalogue organises them by attacker goal and concrete exploitation technique. **Required coverage per task plan:** 1. Unauthorized data access via UUID guessing 2. Double-commitment race 3. Stale decision replay 4. Scope elevation 5. Relationship flood 6. Webhook SSRF 7. Oversized payload DoS 8. Decision policy bypass 9. Audit trail poisoning 10. Token audience confusion --- ## AC-01 — Unauthorized Data Access via UUID Guessing **Goal:** Access CapacityOffer or LoadRequest records belonging to another organization. **Preconditions:** - Attacker holds a valid JWT with `capacity:read` or `loads:read` scope for their own organization. - Attacker knows the general UUID format used by the broker. **Attack steps:** 1. Observe one or more resource IDs from legitimate API responses (e.g., own CapacityOffer IDs). 2. Attempt to infer ID generation patterns (sequential counters, timestamp-based prefixes). 3. Enumerate IDs by issuing GET requests with mutated or incremented UUIDs. 4. For UUIDv7 IDs, attempt timestamp-range scanning: generate all UUIDv7 values for a known creation time window and iterate. 5. If any guessed ID returns data, read confidential counterparty payload. **Protocol controls:** - All resource IDs are UUIDv7 with a randomly generated 62-bit node/clock-seq component; the probability of a valid guess for a single request is ≈ 2⁻⁶² ≈ 2.2 × 10⁻¹⁹. - Every resource GET filters on `ownerOrganizationId` derived from the token's `org_id` claim; a correct UUID belonging to another org returns `404 Not Found` (never `403`), per R-001. - Brokers MUST apply rate limiting on read endpoints (per-org, per-token); aggressive enumeration triggers `429 Too Many Requests` before a statistically meaningful number of IDs can be tested. **Residual risk:** NEGLIGIBLE — the combination of 62-bit entropy and 404-not-403 renders systematic guessing computationally infeasible and observationally indistinguishable from legitimate not-found responses. **Related threats:** T-06 (Resource Enumeration Resistance), API1. --- ## AC-02 — Double-Commitment Race **Goal:** Commit the same MatchDecision twice, creating duplicate binding records or bypassing bilateral approval requirements. **Preconditions:** - Attacker controls one side of a bilateral MatchDecision (e.g., the initiator organization). - Attacker can issue concurrent HTTP requests. **Attack steps:** 1. Obtain a MatchDecision in `PENDING_INITIATOR_ACCEPT` state. 2. Issue two or more simultaneous COMMIT (or ACCEPT) requests with the same payload. 3. If the broker uses non-serialized writes, both requests may pass a read-check-write sequence concurrently before either commits, causing two state transitions. 4. Alternatively, issue one ACCEPT and immediately one COMMIT before the ACCEPT is persisted, attempting to skip the bilateral approval gate. **Protocol controls:** - Every mutating state transition on a MatchDecision MUST use a compare-and-swap on `resourceVersion`; only one transition can win the CAS; all others receive `409 Conflict`. - The `resourceVersion` field is a monotonically increasing integer; the client MUST supply the current version in the request body; mismatches are rejected. - Idempotency keys are REQUIRED on all mutating requests; a duplicate idempotency key within the idempotency window (24 h) returns the original result without re-executing. - State machine is defined such that a COMMIT from state `PENDING_ACCEPTOR_ACCEPT` is rejected; the machine enforces ordering regardless of concurrency. **Residual risk:** LOW — CAS + idempotency keys eliminate the double-commit window. **Related threats:** T-12 (Double-Commitment Race), API5. --- ## AC-03 — Stale Decision Replay **Goal:** Re-apply an old ACCEPT or COMMIT action on a MatchDecision that has since moved to a terminal state (REJECTED, EXPIRED, or COMMITTED), resurrecting a dead commitment. **Preconditions:** - Attacker captured a valid signed ACCEPT request from a prior session. - The MatchDecision has since been REJECTED by the counterparty or has EXPIRED. **Attack steps:** 1. Re-send the captured ACCEPT request verbatim, using the original idempotency key, or with a new idempotency key to bypass the idempotency cache. 2. If the broker does not validate terminal state transitions, the decision is incorrectly re-activated. 3. Attacker now holds a fraudulently re-activated commitment. **Protocol controls:** - State machine explicitly defines terminal states (`REJECTED`, `EXPIRED`, `COMMITTED`, `CANCELLED`). Transitions into or from terminal states are PROHIBITED; any attempt returns `409 Conflict` with `currentState` in the error body. - The `resourceVersion` in the replayed request will not match the current version of the terminal record; CAS rejects the transition. - Idempotency key cache stores the terminal outcome; a replay within the cache window returns the original rejected/expired response. - Webhook delivery timestamps are validated against a 300 s window; replayed webhook events are discarded. **Residual risk:** LOW — terminal state + versioned CAS prevents resurrection. **Related threats:** T-04 (Replay Resistance), API5. --- ## AC-04 — Scope Elevation **Goal:** Perform an operation (e.g., COMMIT a MatchDecision, write an OutcomeReport) using a token that was granted only a lower-privilege scope (e.g., `matches:read`). **Preconditions:** - Attacker holds a valid JWT with a read-only scope. - Attacker understands the API surface sufficiently to craft valid request payloads. **Attack steps:** 1. Obtain a `matches:read` token (e.g., through a compromised read-only service account). 2. Issue a POST to `/match-decisions/{id}/accept` with a valid body, expecting the broker to validate only authentication and not scope. 3. If scope validation is missing or misimplemented (e.g., only checking for any valid token), the ACCEPT succeeds with a read scope. **Protocol controls:** - Each endpoint declares a **minimum required scope** in the authorization matrix; the middleware MUST reject requests where the token's `scope` claim does not include the required scope before any handler logic runs. - Scope check is performed BEFORE ownership check to avoid information leakage about valid IDs. - Scope strings are exact-match only; `matches:read` does NOT imply `matches:decide`. - The 9 PCX scopes form a non-hierarchical flat set; there is no implicit scope inheritance. **Residual risk:** LOW — flat non-hierarchical scope set eliminates implicit elevation. **Related threats:** T-03 (Token Audience and Scope Validation), API5. --- ## AC-05 — Relationship Flood **Goal:** Exhaust broker storage and processing capacity by creating a large number of PartnerRelationship invite records, consuming quota or triggering notification storms. **Preconditions:** - Attacker holds a valid JWT with `relationships:manage` scope. - Attacker can generate many distinct target `organizationId` values (real or fabricated). **Attack steps:** 1. In a tight loop, issue POST `/partner-relationships` with a different target organization ID per request. 2. Each invite creates a pending PartnerRelationship record and triggers a notification to the target organization. 3. At scale, this floods the target organization's notification inbox and consumes broker storage quota. 4. Alternatively, flood the same target with repeated invites after each is declined. **Protocol controls:** - Per-organization rate limit on `relationships:manage` write operations (normative minimum: 10 invites per minute, 50 per hour, configurable by broker). - Broker MAY enforce a maximum number of PENDING outbound relationships per organization (recommended cap: 100). - Duplicate invite detection: a second invite to the same target organization while one is already PENDING MUST return `409 Conflict` rather than creating a second record. - Invite payload is size-limited (64 KiB maximum body); no large embedded documents. **Residual risk:** LOW — rate limits and duplicate detection prevent flood at protocol level; brokers tune thresholds per deployment. **Related threats:** T-08, API4 (Unrestricted Resource Consumption). --- ## AC-06 — Webhook SSRF **Goal:** Use the broker's webhook delivery infrastructure as a proxy to reach internal services (metadata APIs, internal databases, internal admin endpoints) that are inaccessible from the attacker's network. **Preconditions:** - Attacker holds a valid JWT with `relationships:manage` scope (webhook URL is set during PartnerRelationship creation or update). - Attacker knows or can guess an internal URL (e.g., `http://169.254.169.254/latest/meta-data/` for AWS IMDSv1, or `http://10.0.0.1/admin`). **Attack steps:** 1. Register a PartnerRelationship with `webhookUrl` set to an internal endpoint URL. 2. Trigger a protocol event (e.g., capacity published) that causes the broker to deliver a webhook POST to the configured URL. 3. If the broker does not validate the URL, the POST reaches the internal endpoint. 4. Observe the webhook delivery response payload (echoed back in delivery logs or API response) to exfiltrate internal service responses. **Protocol controls:** - Webhook URL validation at registration time MUST: - Reject `http://` scheme (HTTPS only). - Resolve the hostname and reject if it resolves to: RFC 1918 ranges (10/8, 172.16/12, 192.168/16), loopback (127/8, ::1), link-local (169.254/16, fe80::/10), or cloud metadata addresses (169.254.169.254, fd00:ec2::254). - Reject URLs with embedded credentials (`https://user:pass@host/`). - DNS re-resolution at delivery time with the same blocklist (prevents DNS rebinding). - HTTP redirect following is PROHIBITED; `3xx` responses cause delivery failure. - Delivery response body is NOT echoed back to the registering organization in API responses or logs (prevents exfiltration). - Delivery is performed by a dedicated egress process with no access to internal credential stores or metadata endpoints. **Residual risk:** MEDIUM — DNS rebinding with short TTLs remains a theoretical vector; bounded by time-limited re-validation on each delivery attempt (TTL floor: 60 s) and isolated egress network segment recommendation. **Related threats:** T-05 (Webhook Authentication), API7 (SSRF). --- ## AC-07 — Oversized Payload DoS **Goal:** Exhaust broker memory, CPU, or storage by submitting extremely large JSON payloads, causing out-of-memory errors, parser slowdowns, or disk exhaustion. **Preconditions:** - Attacker holds any valid JWT with a write scope. **Attack steps:** 1. Construct a JSON body significantly larger than a legitimate resource payload (e.g., 50 MB CapacityOffer with a large `notes` string or deeply nested `externalReferences` array). 2. POST to any write endpoint (e.g., `POST /capacity-offers`). 3. If the broker reads the full body before validating size, the parser allocates memory for the entire payload, causing memory pressure or OOM. 4. Repeat concurrently from multiple tokens to amplify impact. **Protocol controls:** - API boundary enforces maximum request body size before parsing: - Standard resources: 256 KiB. - PartnerRelationship payloads: 64 KiB. - Requests exceeding limits receive `413 Payload Too Large` immediately without body parsing. - `externalReferences` array is limited to a maximum of 20 items; each item is a string of maximum 512 characters. - Per-organization write rate limits prevent sustained flood from a single token. - Schema validation rejects unexpected deeply-nested structures before any business logic runs. **Residual risk:** LOW — body-size enforcement at the boundary prevents parser exploitation. **Related threats:** T-08 (Oversized Payload Handling), API4. --- ## AC-08 — Decision Policy Bypass **Goal:** Commit a MatchDecision without obtaining the counterparty's consent, by exploiting a misconfigured or improperly enforced decision policy. **Preconditions:** - Attacker controls one organization in a bilateral PartnerRelationship. - The PartnerRelationship uses `BILATERAL_REQUIRED` decision policy (the default). **Attack steps:** 1. Create a MatchDecision as the initiator. 2. Issue two ACCEPT requests from two different member accounts of the **same** organization (not the counterparty), attempting to satisfy the "two-party acceptance" check using own members. 3. Alternatively, manipulate the PartnerRelationship `decisionPolicy` field after the relationship is established, changing it from `BILATERAL_REQUIRED` to `INITIATOR_ONLY`, then issue a single ACCEPT to commit. 4. Alternatively, find an endpoint that applies the wrong policy for a given decision or skips policy validation on the COMMIT path. **Protocol controls:** - Decision policy is immutable after PartnerRelationship activation; updates to `decisionPolicy` are REJECTED with `422 Unprocessable Entity` once the relationship is in `ACTIVE` state. - BILATERAL_REQUIRED state machine evaluates acceptance against the two **distinct** organization IDs stored at MatchDecision creation time, not against the calling token's `org_id`. - A second ACCEPT from the same `organizationId` as the first ACCEPT is idempotent and does not advance the state to COMMITTED; state only advances when a distinct second `organizationId` submits ACCEPT. - COMMIT is not a distinct endpoint; it is the implicit result of the second distinct ACCEPT under BILATERAL_REQUIRED. Brokers MUST NOT expose a separate COMMIT endpoint that bypasses policy evaluation. **Residual risk:** LOW — policy immutability and org-ID-based evaluation eliminate the bypass paths above. **Related threats:** T-12, API5 (Broken Function Level Authorization), API6. --- ## AC-09 — Audit Trail Poisoning **Goal:** Insert false, modified, or deleted entries into the audit log to conceal unauthorized actions or frame a legitimate organization. **Preconditions:** - Attacker has write access to the audit log via application-layer API (no direct DB access). - Alternatively, attacker has compromised an application service account. **Attack steps:** 1. Issue API calls that normally generate audit events, then attempt to DELETE or PATCH the corresponding audit log entries via the API. 2. Submit a crafted OutcomeReport with a forged timestamp to backdate an event. 3. Issue audit events using a legitimate token to falsely attribute actions to a different organization member. 4. Flood the audit log to push older genuine entries out of the retention window. **Protocol controls:** - Audit log entries are **append-only**; there is no DELETE or PATCH endpoint for audit records. - Audit entries are written by the protocol service itself, not accepted from clients; clients cannot inject audit entries directly. - Each audit entry includes the server-generated `createdAt` timestamp and the token's `sub` claim; client-supplied timestamps are ignored. - Audit entries include a cryptographic chaining hash (SHA-256 of previous entry + current payload) to detect gaps or insertions. - `outcomes:review` scope provides read-only access to audit records; no write scope exists for the audit log API. - Retention window is enforced by the broker; volume-based flooding is bounded by write rate limits and storage quotas. **Residual risk:** LOW — append-only + chained hashes make tampering detectable. **Related threats:** T-09 (Sensitive Log Redaction), API6. --- ## AC-10 — Token Audience Confusion **Goal:** Use a JWT issued for one PCX service (e.g., a reporting service) to authenticate against a different PCX service (e.g., the core broker API), gaining unintended access. **Preconditions:** - Attacker holds a valid JWT with a broad or wrong `aud` claim. - Multiple PCX services share the same signing key or the broker does not validate `aud`. **Attack steps:** 1. Obtain a JWT issued for `aud: pcx-reporting.example.com` with broad scopes. 2. Present this token to the core broker API at `pcx-broker.example.com`. 3. If the broker validates signature only (not `aud`), the token is accepted and the attacker gains access to broker resources. 4. Alternatively, obtain a JWT issued for an internal service-to-service call (e.g., a batch job with `capacity:publish` scope) and use it to publish capacity offers on behalf of an organization the batch job should not control. **Protocol controls:** - The broker MUST validate that the `aud` claim contains its own service identifier (a non-guessable URL or URN configured at deployment time). - Tokens with `aud` that does not exactly match (or contain) the broker's expected audience are rejected with `401 Unauthorized` before scope or org checks run. - Service-to-service internal tokens MUST use a separate `aud` value that is not accepted by the external broker API. - JWKS endpoint is pinned at configuration time; dynamically discovered JWKS endpoints are PROHIBITED without explicit allow-listing. - The `org_id` claim in the token ties every authorized action to a specific organization; even a valid broad-scope token cannot act for an organization it was not issued for. **Residual risk:** LOW — strict `aud` validation at the gateway eliminates cross-service reuse. **Related threats:** T-03 (Token Audience and Scope Validation), API2 (Broken Authentication). --- ## Risk Summary | AC ID | Abuse Case | Residual Risk | Status | | ------- | ----------- | --------------- | -------- | | AC-01 | UUID guessing | NEGLIGIBLE | ACCEPTED | | AC-02 | Double-commitment race | LOW | ACCEPTED | | AC-03 | Stale decision replay | LOW | ACCEPTED | | AC-04 | Scope elevation | LOW | ACCEPTED | | AC-05 | Relationship flood | LOW | ACCEPTED | | AC-06 | Webhook SSRF | MEDIUM | ACCEPTED — DNS rebinding residual; time-bounded re-validation required | | AC-07 | Oversized payload DoS | LOW | ACCEPTED | | AC-08 | Decision policy bypass | LOW | ACCEPTED | | AC-09 | Audit trail poisoning | LOW | ACCEPTED | | AC-10 | Token audience confusion | LOW | ACCEPTED | **No unresolved CRITICAL or HIGH findings.** The single MEDIUM residual risk (AC-06 DNS rebinding) is accepted with the requirement for a 60-second TTL floor and an isolated egress network segment. This risk is reviewed at M002 (webhook delivery implementation milestone). --- # OpenFreyt Protocol — Authorization Matrix Source: security/authorization-matrix.md **Status:** Normative **Version:** 1.0.0-draft **Locked by:** M001-S01-T02 **Requirement refs:** R-001 (404-vs-403 rule), R-005 (field-level disclosure) This document is the authoritative access-control specification for all PCX protocol operations. Every broker implementation **MUST** enforce all rules stated here. The matrix governs nine authorization scopes, seven resources, and four roles. --- ## 1. Cross-Cutting Rules ### 1.1 404-vs-403 Rule (R-001) When a requester lacks authorization to **know a resource exists**, the server **MUST** respond with **HTTP 404** (Not Found), not HTTP 403 (Forbidden). **Rationale:** returning 403 confirms the resource exists and enables enumeration attacks. Applying 404 universally removes this signal. **Application:** this rule applies to all read, mutate, and delete operations on all seven resources. The broker evaluates existence and authorization simultaneously; if authorization fails, the response is 404 regardless of whether the resource actually exists. **Exceptions:** - If the **requesting token belongs to the owning organization** and the resource exists, the broker **MAY** return 403 to distinguish "resource exists but operation is forbidden" from "resource not found". This is an implementation option; 404 is always permissible. - System-level errors (malformed IDs, invalid scopes) return 400 or 401 as appropriate regardless of resource existence. ### 1.2 Token Validity Preconditions All requests require a valid, non-expired bearer token issued by the authoritative broker for the resource namespace. Tokens **MUST** include: | Claim | Requirement | | --- | --- | | `iss` | Broker service identifier (URI) | | `sub` | Subject (organizationId or service account ID) | | `aud` | Broker service identifier (exact match required) | | `scope` | Space-separated list of granted PCX scopes | | `exp` | Expiry timestamp (MUST be in future) | | `iat` | Issue timestamp | A token with an `aud` that does not exactly match the receiving broker is rejected with HTTP 401 (audience confusion protection). ### 1.3 Scope Hierarchy Scopes are discrete; there is no implicit hierarchy. Possessing `capacity:publish` does **not** imply `capacity:read`. Each operation requires exactly the scopes listed in the matrix below. ### 1.4 Broker-Scoped Authority All scopes are **broker-scoped**: a token issued by Broker A grants no rights on Broker B. Cross-broker operations are not defined in PCX 1.x. ### 1.5 Relationship Preconditions Several operations require an `active` PartnerRelationship between the requesting organization (`sub` claim) and the resource-owning organization (`ownerOrganizationId` field). "Active" means the PartnerRelationship resource has state `active` and has not been suspended or terminated. A pending, suspended, or terminated relationship does **not** satisfy a relationship precondition. --- ## 2. Roles | Role | Identity Source | Auth Flow | | --- | --- | --- | | `BrokerService` | Platform operator; has elevated internal authority | Service token, internally signed | | `OrganizationService` | Machine client (TMS/ERP) acting for an org | OAuth 2.0 Client Credentials | | `OrganizationMember` | Human user affiliated with an org | OAuth 2.0 Authorization Code + PKCE | | `ComplianceReviewer` | Human or service performing compliance review | OAuth 2.0 Authorization Code or Client Credentials | --- ## 3. Scope-to-Resource Authorization Matrix The table below maps each **(scope × resource × role)** combination to the preconditions required. Columns: - **Allowed Roles** — roles that may hold this scope - **Ownership precondition** — whether `ownerOrganizationId` must match the token `sub` - **Relationship precondition** — whether an `active` PartnerRelationship with the owner is required - **Visibility rule** — what the requester may see (field-level detail in §4) ### 3.1 `relationships:manage` — PartnerRelationship Operations: create invitation, accept invitation, update permitted scopes, suspend, terminate. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Create (invite) | OrganizationService, OrganizationMember, BrokerService | Token `sub` becomes `initiatorOrganizationId` | None (creating the relationship) | Own record only | | Accept | OrganizationService, OrganizationMember, BrokerService | Token `sub` must equal `acceptorOrganizationId` | None (the relationship is `pending`) | Own record only | | Read own | OrganizationService, OrganizationMember, BrokerService | Token `sub` equals initiator or acceptor | N/A | Full fields | | Update permitted scopes | OrganizationService, OrganizationMember, BrokerService | Token `sub` equals initiator or acceptor | Must be `active` | — | | Suspend | OrganizationService, OrganizationMember, BrokerService | Token `sub` equals initiator or acceptor | Must be `active` | — | | Terminate | OrganizationService, OrganizationMember, BrokerService | Token `sub` equals initiator or acceptor | Any state | — | | List (all visible) | OrganizationService, OrganizationMember, BrokerService | Token `sub` is party to each result | N/A | Full fields on own records | | Admin read-any | BrokerService | None | None | Full fields | **ComplianceReviewer** does **not** hold `relationships:manage`; reviewers have read-only visibility to relationships they are party to via `outcomes:review`. ### 3.2 `capacity:publish` — CapacityOffer Operations: create, update, withdraw, cancel, expire (server-side). | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Create | OrganizationService, OrganizationMember | Token `sub` becomes `ownerOrganizationId` | None | Own record full fields | | Update | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | Own record full fields | | Withdraw / Cancel | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | — | | Read own | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | Full fields including pre-match sensitive fields | | Admin mutate | BrokerService | None | None | Full fields | **Ownership is mandatory**: an organization may only publish, update, or retract its own CapacityOffers. ### 3.3 `capacity:read` — CapacityOffer Operations: read, list, search. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Read partner offer | OrganizationService, OrganizationMember | Token `sub` ≠ `ownerOrganizationId` | `active` PartnerRelationship with `ownerOrganizationId`; scope `capacity:read` must be in `permittedScopes` | Pre-match restricted (§4) | | Read own offer | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | Full fields | | List / Search | OrganizationService, OrganizationMember | N/A | Each result filtered: only items where relationship precondition is satisfied appear | Pre-match restricted per item | | Admin read-any | BrokerService | None | None | Full fields | If the relationship precondition is not met, the resource **MUST** be omitted from list/search results (treated as if it does not exist — 404 rule applied at collection level). ### 3.4 `loads:publish` — LoadRequest Operations: create, update, withdraw, cancel. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Create | OrganizationService, OrganizationMember | Token `sub` becomes `ownerOrganizationId` | None | Own record full fields | | Update | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | Own record full fields | | Withdraw / Cancel | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | — | | Read own | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | Full fields | | Admin mutate | BrokerService | None | None | Full fields | ### 3.5 `loads:read` — LoadRequest Operations: read, list, search. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Read partner request | OrganizationService, OrganizationMember | Token `sub` ≠ `ownerOrganizationId` | `active` PartnerRelationship with `ownerOrganizationId`; scope `loads:read` must be in `permittedScopes` | Pre-match restricted (§4) | | Read own request | OrganizationService, OrganizationMember | Token `sub` = `ownerOrganizationId` | None | Full fields | | List / Search | OrganizationService, OrganizationMember | N/A | Filtered per-item as above | Pre-match restricted per item | | Admin read-any | BrokerService | None | None | Full fields | ### 3.6 `matches:read` — MatchProposal, MatchDecision Operations: read, list proposals/decisions. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Read MatchProposal | OrganizationService, OrganizationMember | Token `sub` equals org owning capacity offer or load request within the proposal | Active relationship between the two parties | Full proposal fields (counterparty identity revealed post-proposal-creation) | | Read MatchDecision | OrganizationService, OrganizationMember | Token `sub` is party to the matched resources | Active relationship | Full fields | | List proposals | OrganizationService, OrganizationMember | N/A | Filtered to proposals where requester is a party | Full fields per item | | ComplianceReviewer read | ComplianceReviewer | None | None | Full fields on proposals/decisions in assigned scope | | Admin read-any | BrokerService | None | None | Full fields | ### 3.7 `matches:decide` — MatchProposal (transition to accepted/rejected) Operations: submit PartyDecision, trigger state machine transition. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Submit decision | OrganizationService, OrganizationMember | Token `sub` is party to the proposal (capacity or load owner) | Active relationship | — | | Override decision (admin) | BrokerService | None | None | — | **Decision policy enforcement** (from DecisionPolicy enum defined in Glossary §1.4): | Policy | Required decisions before `accepted` | | --- | --- | | `BILATERAL_REQUIRED` | Both capacity owner and load owner must submit `accept` | | `INITIATOR_ONLY` | Only the organization that initiated the MatchProposal request must accept | | `ACCEPTOR_ONLY` | Only the non-initiating organization must accept | A party **MUST NOT** be permitted to submit multiple decisions on the same proposal (idempotent `accept` re-submission is allowed; changing from `accept` to `reject` after the proposal is `accepted` is not allowed and returns HTTP 409). ### 3.8 `outcomes:write` — ShipmentExecution, OutcomeRecord Operations: create/update ShipmentExecution, submit OutcomeRecord. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Create ShipmentExecution | OrganizationService, OrganizationMember | Token `sub` = carrier org (capacity owner of matched offer) | Active MatchDecision must exist | Own record full fields | | Update ShipmentExecution | OrganizationService, OrganizationMember | Token `sub` = carrier org | Active MatchDecision must exist | Own record full fields | | Submit OutcomeRecord | OrganizationService, OrganizationMember | Token `sub` = carrier org or broker | Active ShipmentExecution must exist | Own record full fields | | Admin write | BrokerService | None | None | Full fields | An OutcomeRecord **MUST NOT** be submitted unless an `active` MatchDecision exists referencing the same CapacityOffer/LoadRequest pair. ### 3.9 `outcomes:review` — OutcomeRecord (compliance review) Operations: read OutcomeRecord, update review status, add compliance notes. | Operation | Allowed Roles | Ownership Precondition | Relationship Precondition | Visibility Rule | | --- | --- | --- | --- | --- | | Read OutcomeRecord | ComplianceReviewer, BrokerService | None | ComplianceReviewer must be assigned or broker-affiliated | Full fields including audit fields | | Update review status | ComplianceReviewer, BrokerService | None | ComplianceReviewer must be assigned or broker-affiliated | — | | List pending reviews | ComplianceReviewer, BrokerService | None | Filtered to assigned/unassigned in broker scope | Full fields | | Cancel MatchDecision (compliance) | BrokerService | None | None | — | Only `BrokerService` may **cancel** a MatchDecision after it reaches `active` state (e.g., for fraud, compliance failure). `ComplianceReviewer` surfaces the finding; the broker acts. --- ## 4. Field-Level Disclosure (R-005) Certain fields on CapacityOffer and LoadRequest are **sensitive** and **MUST** be suppressed during the **pre-match** phase when accessed by a non-owning organization. The pre-match phase is defined as: after a resource is `published` but before a `MatchDecision` with state `active` links the requesting organization to the resource's owning organization. ### 4.1 CapacityOffer — Suppressed Pre-Match Fields The following fields are **suppressed** (replaced with `null` or omitted) when a non-owning organization reads a CapacityOffer before an active MatchDecision exists: | Field | Pre-Match Visible | Post-Match Visible | Scope Required to Unlock | | --- | --- | --- | --- | | `ownerOrganizationId` | **No** — suppressed | Yes | `matches:read` + active MatchDecision | | `carrierIdentifier` (if present) | **No** | Yes | `matches:read` + active MatchDecision | | `exactPrice` / `pricePerKm` | **No** | Yes | `matches:read` + active MatchDecision | | `driverContact` | **No** | Yes | `outcomes:write` + active ShipmentExecution | | `vehicleLicensePlate` | **No** | Yes | `outcomes:write` + active ShipmentExecution | | `origin` (full address) | Partial — region only | Full | `matches:read` + active MatchDecision | | `destination` (full address) | Partial — region only | Full | `matches:read` + active MatchDecision | | `departureAt` (exact time) | Partial — date only | Full | `capacity:read` (date) + `matches:read` (exact time) | Fields **always visible** to a relationship-authorized reader: `id`, `resourceVersion`, `bodyType`, `capacityKg`, `capacityM3`, `availableFrom` (date), `region` (adminRegionCode), `state`, `createdAt`, `updatedAt`. ### 4.2 LoadRequest — Suppressed Pre-Match Fields | Field | Pre-Match Visible | Post-Match Visible | Scope Required to Unlock | | --- | --- | --- | --- | | `ownerOrganizationId` | **No** — suppressed | Yes | `matches:read` + active MatchDecision | | `shipperIdentifier` | **No** | Yes | `matches:read` + active MatchDecision | | `exactBudget` / `maxRatePerKm` | **No** | Yes | `matches:read` + active MatchDecision | | `consigneeContact` | **No** | Yes | `outcomes:write` + active ShipmentExecution | | `pickupAddress` (full) | Partial — region only | Full | `matches:read` + active MatchDecision | | `deliveryAddress` (full) | Partial — region only | Full | `matches:read` + active MatchDecision | | `requestedPickupAt` (exact) | Partial — date only | Full | `loads:read` (date) + `matches:read` (exact time) | Fields **always visible** to a relationship-authorized reader: `id`, `resourceVersion`, `cargoType`, `weightKg`, `volumeM3`, `pickupRegion` (adminRegionCode), `deliveryRegion` (adminRegionCode), `requiredBodyTypes`, `state`, `createdAt`, `updatedAt`. ### 4.3 MatchProposal — Field Visibility When a MatchProposal is **created**, both parties (capacity owner and load owner) **MAY** read the full MatchProposal including the counterparty's resource reference (capacityOfferId, loadRequestId), but **not** the counterparty's organization identity until `matches:read` scope is present and a MatchDecision is active. ### 4.4 OutcomeRecord — Field Visibility OutcomeRecord fields are restricted to parties with `outcomes:write` (for own records) or `outcomes:review` (for compliance). No pre-match suppression applies to OutcomeRecords because they cannot exist without an active MatchDecision. --- ## 5. Scope × Role Eligibility This table specifies which roles are **eligible** to be granted each scope. A BrokerService token may carry any scope by design; eligibility applies to externally-issued tokens. | Scope | BrokerService | OrganizationService | OrganizationMember | ComplianceReviewer | | --- | --- | --- | --- | --- | | `relationships:manage` | ✓ (full) | ✓ | ✓ | ✗ | | `capacity:publish` | ✓ | ✓ | ✓ | ✗ | | `capacity:read` | ✓ | ✓ | ✓ | ✗ | | `loads:publish` | ✓ | ✓ | ✓ | ✗ | | `loads:read` | ✓ | ✓ | ✓ | ✗ | | `matches:read` | ✓ | ✓ | ✓ | ✓ | | `matches:decide` | ✓ | ✓ | ✓ | ✗ | | `outcomes:write` | ✓ | ✓ | ✓ (carrier member only) | ✗ | | `outcomes:review` | ✓ | ✗ | ✗ | ✓ | **Notes:** - `outcomes:write` for `OrganizationMember` is further constrained to members of the carrier organization (capacity owner). Broker implementations **MUST** enforce this at token-issuance time. - `outcomes:review` is **never** granted to `OrganizationService` or `OrganizationMember` acting in a non-compliance role. It is a dedicated, separately-provisioned role. - `ComplianceReviewer` receiving `matches:read` may only access proposals and decisions within their assigned broker scope. --- ## 6. Combined Resource × Scope Summary The table below summarizes which scope governs each resource's write and read operations. "—" means the scope does not apply to that resource. | Resource | Write Scope | Read Scope | Decide Scope | Review Scope | | --- | --- | --- | --- | --- | | PartnerRelationship | `relationships:manage` | `relationships:manage` | — | — | | CapacityOffer | `capacity:publish` | `capacity:read` | — | — | | LoadRequest | `loads:publish` | `loads:read` | — | — | | MatchProposal | BrokerService only | `matches:read` | `matches:decide` | `matches:read` (ComplianceReviewer) | | MatchDecision | Auto-created (BrokerService) | `matches:read` | — | `outcomes:review` | | ShipmentExecution | `outcomes:write` | `outcomes:write` (own), `outcomes:review` | — | `outcomes:review` | | OutcomeRecord | `outcomes:write` | `outcomes:write` (own), `outcomes:review` | — | `outcomes:review` | --- ## 7. Authorization Enforcement Algorithm Broker implementations **MUST** evaluate authorization in this sequence for every request: ```text 1. Validate token signature and expiry → 401 on failure 2. Validate token audience (aud = this broker) → 401 on mismatch 3. Parse organizationId from token sub claim 4. Check required scope is in token scope list → 401 on missing scope 5. Determine resource existence (internal lookup, not exposed to caller yet) 6. Evaluate ownership precondition: a. If ownership required AND sub ≠ ownerOrganizationId → skip to step 7 b. If ownership matches → grant full-owner access 7. Evaluate relationship precondition (if applicable): a. Load active PartnerRelationship between sub and ownerOrganizationId b. If no active relationship → respond 404 (R-001) c. If relationship active but required scope not in permittedScopes → 404 8. Resource does not exist → respond 404 9. Grant access; apply field-level disclosure rules (§4) ``` **Key property:** steps 5–8 all result in 404, never 403, for non-owning requesters. This prevents enumeration of both resource existence and authorization failures. --- ## 8. Audit Requirements Every operation that **mutates** a resource (create, update, state transition, decision submission, cancellation) **MUST** produce an immutable audit log entry containing: | Field | Value | | --- | --- | | `eventId` | UUIDv7 | | `occurredAt` | ISO 8601 timestamp with millisecond precision | | `actorOrganizationId` | Token `sub` value | | `actorRole` | Role extracted from token | | `resourceType` | One of the 7 PCX resource types | | `resourceId` | UUIDv7 of the affected resource | | `operation` | Verb (create, update, withdraw, decide, etc.) | | `scopeUsed` | The PCX scope from the request token | | `outcome` | `permitted` or `denied` | Audit entries for **denied** operations **MUST** be produced **before** the 404 response is returned. Denied audit entries **MUST NOT** include the resource payload. --- ## 9. Normative References | Ref | Document | | --- | --- | | R-001 | `requirements/requirements.yaml` — 404-vs-403 rule | | R-005 | `requirements/requirements.yaml` — field-level disclosure | | Glossary §4 | `specification/glossary.md` — scope definitions | | Glossary §5 | `specification/glossary.md` — role definitions | | Glossary §7 | `specification/glossary.md` — key terms (pre-match, post-match, relationship-gated) | | OWASP API Top 10 | `security/threat-model.md` — threat mapping | | Abuse Cases | `security/abuse-cases.md` — adversarial scenarios for each authorization rule | --- # Security Artifacts Source: security/README.md ## Required (foundation gate §13) - `threat-model.md` — threat model covering the attack surface of the protocol - `authorization-matrix.md` — complete matrix of which roles can perform which operations on which resources - `abuse-cases.md` — abuse case catalogue (adversarial perspective) These artifacts must exist and be reviewed before any production broker implementation begins. See QUALITY-STRATEGY.md §8.10 and §13. ## Threat model scope - Organization and object-level isolation - Field-level disclosure - Token audience and scope validation - Replay resistance - Webhook authentication - Resource enumeration resistance - Injection and hostile references - Oversized payload handling - Sensitive log redaction - Retention and deletion - Software supply chain - OWASP API Security Top 10 No unresolved critical or high finding is releasable without an explicit, time-bounded risk acceptance. --- # OpenFreyt Protocol — Threat Model Source: security/threat-model.md **Status:** Normative **Version:** 1.0.0-draft **Locked by:** M001-S01-T03 **Last reviewed:** 2026-08-31 --- ## 1. Scope This threat model covers the attack surface of the OpenFreyt Protocol (PCX) specification and any conformant broker implementation. It does **not** cover infrastructure-level concerns (OS hardening, network topology, key management HSMs) except where the protocol makes normative statements about them. **In-scope components:** - PCX REST/JSON API surface (all 9 authorization scopes) - Token validation and audience binding - Resource lifecycle state machines - Webhook delivery infrastructure - Audit log subsystem - Organization / isolation boundaries **Out-of-scope:** - Physical datacenter security - TLS certificate lifecycle (assumed: TLS 1.3 minimum, valid certs) - Internal broker-to-broker transport (not yet specified) --- ## 2. Assets | Asset ID | Asset | Confidentiality | Integrity | Availability | | ---------- | ------- | ----------------- | ----------- | -------------- | | A-01 | PartnerRelationship records | HIGH | CRITICAL | HIGH | | A-02 | CapacityOffer payload (price, volume, route) | HIGH | HIGH | HIGH | | A-03 | LoadRequest payload (cargo details, shipper identity) | HIGH | HIGH | HIGH | | A-04 | MatchProposal (counterparty identity revealed post-match) | CRITICAL | CRITICAL | HIGH | | A-05 | MatchDecision and commitment records | CRITICAL | CRITICAL | HIGH | | A-06 | OutcomeReport and compliance data | HIGH | CRITICAL | MEDIUM | | A-07 | Organization identity and legalIdentifier fields | HIGH | HIGH | HIGH | | A-08 | JWT access tokens and scopes | CRITICAL | CRITICAL | HIGH | | A-09 | Audit trail / event log | MEDIUM | CRITICAL | HIGH | | A-10 | Webhook endpoint URLs | MEDIUM | HIGH | MEDIUM | --- ## 3. Trust Boundaries ```text [External Client] --- TLS/JWT ---> [API Gateway] | [Scope + Org Validation] | [Protocol Domain Services] / | \ [Capacity] [Matching] [Outcomes] | [Audit Log] | [Webhook Fan-out] | [External Org Webhooks] (untrusted) ``` Trust is **never** inherited across organization boundaries. A token issued for `OrganizationService:A` grants no authority over resources owned by `OrganizationService:B`, regardless of PartnerRelationship status. --- ## 4. OWASP API Security Top 10 (2023) Mapping ### API1 — Broken Object Level Authorization **Affected components:** All resource endpoints (CapacityOffer, LoadRequest, MatchProposal, MatchDecision, OutcomeReport, PartnerRelationship, Organization). **Attack description:** Attacker increments or guesses resource IDs (UUIDs) to access objects belonging to another organization. **Protocol controls:** - All resource IDs are UUIDv7 (128-bit, random node component) — guessing probability is negligible for individual requests, but sequential scanning must be mitigated at the API layer. - Every resource lookup enforces `ownerOrganizationId` match against the token's `org_id` claim before returning any data. - Non-owning requesters **MUST** receive `404 Not Found` (never `403`) per R-001 and the authorization-matrix 404-vs-403 rule. **Residual risk:** LOW — UUIDv7 provides ~122 bits of effective entropy; 404-not-403 prevents existence confirmation. **Acceptance status:** ACCEPTED — mitigated by design. --- ### API2 — Broken Authentication **Affected components:** API Gateway, token validation, webhook delivery. **Attack description:** Attacker presents expired, tampered, or wrong-audience tokens. Webhook replay of valid delivery payloads without signature. **Protocol controls:** - Tokens MUST carry a specific `aud` claim matching the broker's service identifier; generic bearer tokens are rejected. - Token expiry (`exp`) and issuance time (`iat`) MUST be validated with a max clock skew of 30 s. - Webhook payloads MUST be signed (HMAC-SHA256 or asymmetric) with a shared secret negotiated during PartnerRelationship establishment; replayed payloads rejected via nonce or timestamp window (≤ 300 s). **Residual risk:** LOW — audience binding and webhook signing are normative requirements. **Acceptance status:** ACCEPTED. --- ### API3 — Broken Object Property Level Authorization **Affected components:** MatchProposal, CapacityOffer, LoadRequest field disclosure. **Attack description:** Attacker reads pre-match fields (counterparty `ownerOrganizationId`, contact details, rate breakdowns) that should be hidden until a MatchDecision is committed. **Protocol controls:** - Field-level disclosure rules documented in authorization-matrix.md §5 (R-005). - Pre-match: `ownerOrganizationId` of the counterparty is suppressed; unlocked only by `matches:read` scope combined with an active `MatchDecision` in ACCEPTED state. - Serialization layer MUST strip suppressed fields before sending; reliance on client-side filtering is prohibited. **Residual risk:** LOW — server-side field stripping is required by spec; tested in conformance suite. **Acceptance status:** ACCEPTED. --- ### API4 — Unrestricted Resource Consumption **Affected components:** All write endpoints, webhook fan-out, relationship management. **Attack description:** Attacker floods the broker with CapacityOffers, LoadRequests, or PartnerRelationship invites to exhaust storage or processing capacity. **Protocol controls:** - Broker implementations MUST enforce per-organization rate limits on all write scopes. - Pagination is mandatory on all list endpoints (max page size 100). - Webhook fan-out MUST be asynchronous with bounded retry budgets (max 5 attempts, exponential back-off with jitter, circuit breaker at 50% failure rate over a 5-minute window). - Oversized payloads MUST be rejected at the API boundary: max body size 256 KiB for standard resources, 64 KiB for PartnerRelationship payloads. **Residual risk:** MEDIUM — rate-limit parameters are implementation-specific; the protocol specifies minimum requirements only. Implementors must tune thresholds. **Acceptance status:** ACCEPTED with implementation guidance. --- ### API5 — Broken Function Level Authorization **Affected components:** Decision policy enforcement (BILATERAL_REQUIRED, INITIATOR_ONLY, ACCEPTOR_ONLY), scope checks on state-transition endpoints. **Attack description:** Attacker invokes ACCEPT or COMMIT on a MatchDecision they are not authorized to finalize, or bypasses BILATERAL_REQUIRED by making two calls from the same organization in sequence. **Protocol controls:** - Each MatchDecision records `initiatorOrganizationId` and `acceptorOrganizationId` from the PartnerRelationship at creation time; these are immutable. - Decision policy is evaluated against these stored IDs, not token identity at decision time. - BILATERAL_REQUIRED: both organizations must submit separate ACCEPT actions; a second ACCEPT from the initiator is a no-op (idempotent) and does not advance state. - State-transition endpoints check both the `matches:decide` scope **and** the decision-policy role before executing any state change. **Residual risk:** LOW. **Acceptance status:** ACCEPTED. --- ### API6 — Unrestricted Access to Sensitive Business Flows **Affected components:** MatchDecision COMMIT flow, OutcomeReport submission. **Attack description:** Attacker exploits automated account creation or bulk match commitment to manipulate freight market prices or commit counterparties to spurious loads. **Protocol controls:** - PartnerRelationship activation requires explicit bilateral `relationships:manage` scope actions — no auto-accept. - MatchDecision COMMIT is gated on BILATERAL_REQUIRED by default; INITIATOR_ONLY and ACCEPTOR_ONLY require explicit configuration in the PartnerRelationship. - OutcomeReport requires `outcomes:write` scope, which MUST NOT be granted to automated ingestion accounts without a compliance review (`outcomes:review` scope audit path). **Residual risk:** LOW. **Acceptance status:** ACCEPTED. --- ### API7 — Server Side Request Forgery **Affected components:** Webhook endpoint registration and delivery. **Attack description:** Attacker registers a webhook URL pointing to an internal service (169.254.x.x, 10.x.x.x, 127.x.x.x, metadata endpoints) to exfiltrate internal tokens or trigger internal actions. **Protocol controls:** - Webhook URL validation MUST reject RFC 1918 addresses, loopback addresses, link-local addresses, and cloud metadata endpoints (169.254.169.254, fd00:ec2::254) at registration time. - URL scheme MUST be `https://` (no `http://`, `file://`, `ftp://`, etc.). - DNS resolution at delivery time MUST be re-validated; a URL that resolves to a forbidden range at delivery time causes immediate disablement of the webhook endpoint. - Redirect following is PROHIBITED during webhook delivery; HTTP 3xx responses cause delivery failure. **Residual risk:** MEDIUM — DNS rebinding attacks remain possible in environments with short TTLs. Mitigated by time-bounded re-validation on each delivery attempt. **Acceptance status:** ACCEPTED with time-bounded DNS re-validation requirement. --- ### API8 — Security Misconfiguration **Affected components:** API Gateway, CORS headers, error response bodies. **Attack description:** Verbose error messages reveal internal stack traces, SQL errors, or resource existence. Overly permissive CORS allows cross-origin token exfiltration. **Protocol controls:** - Error responses MUST use the standard PCX error envelope `{code, message, requestId}` and MUST NOT include stack traces, internal identifiers, or query plans. - CORS policy: `Access-Control-Allow-Origin` MUST NOT be `*` for authenticated endpoints. - Security headers MUST include: `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`. **Residual risk:** LOW. **Acceptance status:** ACCEPTED. --- ### API9 — Improper Inventory Management **Affected components:** Deprecated API versions, shadow endpoints. **Attack description:** Attacker discovers and uses deprecated v0 or staging endpoints that lack current authorization controls. **Protocol controls:** - The PCX specification versioning scheme (`X-PCX-Version` header) requires brokers to declare supported versions; deprecated versions MUST return `410 Gone` after a published sunset date. - Specification changelog MUST record all removed fields and endpoints with their sunset dates. **Residual risk:** LOW. **Acceptance status:** ACCEPTED. --- ### API10 — Unsafe Consumption of APIs **Affected components:** Broker-to-broker federation (future), third-party identity providers. **Attack description:** A PCX broker blindly trusts data received from a partner broker or identity provider, leading to injection or privilege escalation. **Protocol controls:** - All inbound data (including counterparty-supplied fields) MUST be validated against PCX schemas before processing. - `externalReferences` array items MUST be treated as opaque strings; brokers MUST NOT follow external URLs embedded in these fields. - Identity provider claims MUST be validated against a pinned JWKS endpoint; dynamic JWKS discovery is PROHIBITED without explicit configuration. **Residual risk:** LOW. **Acceptance status:** ACCEPTED. --- ## 5. Additional Threat Categories (security/README.md §12) ### T-01 — Organization and Object-Level Isolation **ID:** T-01 **Category:** Object-level authorization **Affected component:** All resource endpoints **Attack description:** Cross-organization data leakage via missing `ownerOrganizationId` filter on list queries. **Protocol control:** Every list endpoint MUST filter by the token's `org_id`; missing filter is a CRITICAL conformance failure. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-02 — Field-Level Disclosure **ID:** T-02 **Category:** Property-level authorization **Affected component:** MatchProposal, CapacityOffer, LoadRequest **Attack description:** Pre-match counterparty fields leaked via overly broad serialization. **Protocol control:** Server-side field stripping enforced per authorization-matrix.md §5. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-03 — Token Audience and Scope Validation **ID:** T-03 **Category:** Broken authentication **Affected component:** API Gateway, token middleware **Attack description:** Token issued for service A accepted by service B; token with broad scope accepted for narrow-scope endpoint. **Protocol control:** Strict `aud` claim matching; per-endpoint minimum scope check before handler dispatch. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-04 — Replay Resistance **ID:** T-04 **Category:** Replay attack **Affected component:** MatchDecision state transitions, webhook delivery **Attack description:** Replayed ACCEPT or COMMIT request re-triggers a state transition already executed; replayed webhook payload duplicates business event processing. **Protocol control:** MatchDecision state machine is idempotent on transitions from a given state; idempotency keys REQUIRED on all mutating requests; webhook nonce / timestamp window. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-05 — Webhook Authentication **ID:** T-05 **Category:** SSRF / integrity **Affected component:** Webhook fan-out **Attack description:** Broker delivers to an unauthenticated URL; attacker intercepts delivery or forges events. **Protocol control:** HMAC-SHA256 signing of webhook body (see API7 controls); HTTPS-only URLs. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-06 — Resource Enumeration Resistance **ID:** T-06 **Category:** Information disclosure **Affected component:** All resource GET endpoints **Attack description:** 403 response reveals resource existence to unauthorized caller. **Protocol control:** 404-not-403 rule per R-001; UUIDv7 IDs. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-07 — Injection and Hostile References **ID:** T-07 **Category:** Injection **Affected component:** All write endpoints, `externalReferences` array **Attack description:** Attacker submits SQL injection, XSS payloads, or hostile URLs in free-text or reference fields. **Protocol control:** Schema validation on all inputs; `externalReferences` treated as opaque strings, never followed; output encoding in error messages. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-08 — Oversized Payload Handling **ID:** T-08 **Category:** DoS **Affected component:** All write endpoints **Attack description:** Attacker sends a multi-megabyte JSON body to exhaust parser memory. **Protocol control:** Max body size 256 KiB enforced at API boundary (64 KiB for PartnerRelationship). Requests exceeding limit receive `413 Payload Too Large`. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-09 — Sensitive Log Redaction **ID:** T-09 **Category:** Data leakage via logs **Affected component:** Audit log, application logs **Attack description:** Bearer tokens, `legalIdentifier` values, contact details written to plaintext logs and accessible to log aggregation operators. **Protocol control:** Audit log schema specifies which fields are loggable; token values MUST be replaced with `[REDACTED]`; `legalIdentifier.value` MUST be hashed (SHA-256 truncated to 12 chars) in application logs. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ### T-10 — Retention and Deletion **ID:** T-10 **Category:** Data lifecycle **Affected component:** All resources, audit log **Attack description:** Organization requests deletion of its data but records persist indefinitely due to missing cascade logic. **Protocol control:** PCX specifies a `DELETED` lifecycle state; once a resource reaches `DELETED`, it MUST be excluded from all list results and individual GETs return 404. Physical deletion is implementation-specific but MUST be achievable within the agreed retention window. **Residual risk:** MEDIUM — physical deletion scheduling is implementation-specific. Normative requirement is logical deletion + listing exclusion. **Acceptance status:** ACCEPTED with implementation guidance. --- ### T-11 — Software Supply Chain **ID:** T-11 **Category:** Supply chain **Affected component:** Reference implementation build pipeline **Attack description:** Compromised npm/pip/cargo dependency introduces malicious code. **Protocol control:** Lock files committed for all package managers; SBOM generated at build time; dependency scan in CI (npm audit / dependabot); no `latest` version ranges. **Residual risk:** MEDIUM — supply chain attacks are difficult to fully mitigate at the protocol specification level; mitigated by CI scanning. **Acceptance status:** ACCEPTED with CI gate requirement. --- ### T-12 — Double-Commitment Race **ID:** T-12 **Category:** Race condition / data integrity **Affected component:** MatchDecision COMMIT endpoint **Attack description:** Two concurrent COMMIT requests from different org members arrive simultaneously; both succeed due to missing serialization, creating two committed records. **Protocol control:** COMMIT MUST be implemented using a compare-and-swap or serializable transaction on `resourceVersion`; optimistic lock failure returns `409 Conflict` with the current `resourceVersion`. **Residual risk:** LOW **Acceptance status:** ACCEPTED --- ## 6. Risk Summary | Level | Count | All mitigated? | | ------- | ------- | ---------------- | | CRITICAL | 0 | — | | HIGH | 0 | — | | MEDIUM | 4 | Yes — all have time-bounded acceptance rationale | | LOW | 18 | Yes | **No unresolved CRITICAL or HIGH findings.** All MEDIUM findings are accepted with concrete implementation requirements documented above. The four MEDIUM residual risks are: 1. **T-04 / API4** — Rate-limit parameter tuning is implementation-specific; minimum requirements specified. Reviewed at: M002 broker reference implementation milestone. 2. **T-07 / API7** — DNS rebinding residual; time-bounded re-validation required on each webhook delivery attempt. Reviewed at: M002 webhook delivery implementation. 3. **T-10** — Physical deletion scheduling is implementation-specific. Reviewed at: M003 data lifecycle milestone. 4. **T-11** — Supply chain scan is a CI gate; cannot be fully eliminated at spec level. Reviewed at: each milestone that changes dependencies. --- # OpenFreyt Protocol — RFC 9457 Error Catalogue Source: specification/errors/error-catalogue.md All error responses from the OpenFreyt Protocol API use RFC 9457 Problem Details (`application/problem+json`). Every error document MUST include `type`, `title`, and `status`. Implementors SHOULD include `detail` and `correlationId` to aid debugging and support. Base schema: `specification/schemas/problem-detail.schema.json` --- ## Type URI Convention All protocol-specific error type URIs follow the form: ``` urn:freyt:protocol:error:{code} ``` where `{code}` is the kebab-case identifier defined in this catalogue. Generic HTTP errors (e.g. `about:blank`) are permitted for conditions not listed here. --- ## Error Types ### 1. `validation-error` — HTTP 400 **Type URI:** `urn:freyt:protocol:error:validation-error` **Title:** `Validation Error` **Status:** 400 **Description:** One or more request body fields failed schema or business-rule validation. The `errors` array extension field contains per-field details. **Extension Fields:** | Field | Type | Description | |----------|--------|--------------------------------------------------------------| | `errors` | array | Array of `{ field, message, code }` objects (RFC 6901 pointer path). | **Detail Format:** `"Request body validation failed; see errors for per-field details."` **Example:** ```json { "type": "urn:freyt:protocol:error:validation-error", "title": "Validation Error", "status": 400, "detail": "Request body validation failed; see errors for per-field details.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBA", "errors": [ { "field": "/payload/equipmentType", "message": "Required field missing.", "code": "required" }, { "field": "/payload/origin/locode", "message": "Must be a valid UN/LOCODE.", "code": "format" } ] } ``` --- ### 2. `authentication-required` — HTTP 401 **Type URI:** `urn:freyt:protocol:error:authentication-required` **Title:** `Authentication Required` **Status:** 401 **Description:** The request lacks valid authentication credentials. The caller must obtain a valid bearer token before retrying. **Extension Fields:** none **Detail Format:** `"A valid bearer token is required to access this resource."` **Example:** ```json { "type": "urn:freyt:protocol:error:authentication-required", "title": "Authentication Required", "status": 401, "detail": "A valid bearer token is required to access this resource.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBB" } ``` --- ### 3. `authorization-denied` — HTTP 403 **Type URI:** `urn:freyt:protocol:error:authorization-denied` **Title:** `Authorization Denied` **Status:** 403 **Description:** The caller is authenticated but lacks the required OAuth2 scope or organization-level permission to perform the operation. See also the 404 vs 403 rule: if the resource does not exist and the caller is not a partner, 404 is returned instead of 403 to prevent existence leakage. **Extension Fields:** | Field | Type | Description | |------------------|--------|-----------------------------------------------| | `requiredScope` | string | The OAuth2 scope required for this operation. | **Detail Format:** `"Insufficient scope; '{requiredScope}' is required."` **Example:** ```json { "type": "urn:freyt:protocol:error:authorization-denied", "title": "Authorization Denied", "status": 403, "detail": "Insufficient scope; 'freyt:capacity-offer:write' is required.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBC", "requiredScope": "freyt:capacity-offer:write" } ``` --- ### 4. `resource-not-found` — HTTP 404 **Type URI:** `urn:freyt:protocol:error:resource-not-found` **Title:** `Resource Not Found` **Status:** 404 **Description:** The referenced resource does not exist, or the caller is not in an active partner relationship with the resource owner and the protocol's existence-leakage policy applies (PROTOCOL.md §R-001). **Extension Fields:** | Field | Type | Description | |----------------|--------|-------------------------------------------| | `resourceType` | string | The resource type that was not found. | | `resourceId` | string | The ID that was requested, if safe to echo. | **Detail Format:** `"The requested {resourceType} was not found."` **Example:** ```json { "type": "urn:freyt:protocol:error:resource-not-found", "title": "Resource Not Found", "status": 404, "detail": "The requested CapacityOffer was not found.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBD", "resourceType": "CapacityOffer", "resourceId": "co_01HYZ000000000000000" } ``` > **Note (Existence Leakage):** When a non-partner requests a resource that exists > but belongs to an unrelated organization, the protocol MUST return 404 (not 403) > to avoid leaking the existence of the resource. Implementors MUST NOT vary the > response body between "truly not found" and "hidden" cases. --- ### 5. `relationship-inactive` — HTTP 409 **Type URI:** `urn:freyt:protocol:error:relationship-inactive` **Title:** `Relationship Inactive` **Status:** 409 **Description:** The operation requires an active `PartnerRelationship` between the two organizations, but no active relationship exists. The relationship may be in `draft`, `requested`, `suspended`, or `terminated` state. **Extension Fields:** | Field | Type | Description | |--------------------------|--------|---------------------------------------------------------| | `relationshipId` | string | ID of the relevant PartnerRelationship, if one exists. | | `relationshipState` | string | Current state of the relationship. | | `requiredState` | string | State required for the operation (always `active`). | **Detail Format:** `"An active PartnerRelationship is required; current state is '{relationshipState}'."` **Example:** ```json { "type": "urn:freyt:protocol:error:relationship-inactive", "title": "Relationship Inactive", "status": 409, "detail": "An active PartnerRelationship is required; current state is 'suspended'.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBE", "relationshipId": "pr_01HYZ000000000000001", "relationshipState": "suspended", "requiredState": "active" } ``` --- ### 6. `stale-resource-version` — HTTP 409 **Type URI:** `urn:freyt:protocol:error:stale-resource-version` **Title:** `Stale Resource Version` **Status:** 409 **Description:** The `If-Match` header value does not match the resource's current `resourceVersion`. The caller must fetch the latest version and retry. **Extension Fields:** | Field | Type | Description | |---------------------|--------|---------------------------------------------------| | `currentVersion` | string | The resource's current version (ETag value). | | `submittedVersion` | string | The version submitted by the caller. | **Detail Format:** `"The submitted resourceVersion is stale; fetch the current resource and retry."` **Example:** ```json { "type": "urn:freyt:protocol:error:stale-resource-version", "title": "Stale Resource Version", "status": 409, "detail": "The submitted resourceVersion is stale; fetch the current resource and retry.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBF", "currentVersion": "3", "submittedVersion": "2" } ``` --- ### 7. `idempotency-conflict` — HTTP 409 **Type URI:** `urn:freyt:protocol:error:idempotency-conflict` **Title:** `Idempotency Conflict` **Status:** 409 **Description:** A request with the same `Idempotency-Key` was previously received with a different request payload (fingerprint mismatch). The protocol's idempotency guarantee cannot be satisfied. The caller must use a new `Idempotency-Key` or resend the identical payload. **Extension Fields:** | Field | Type | Description | |--------------------|--------|----------------------------------------------------| | `idempotencyKey` | string | The conflicting idempotency key. | **Detail Format:** `"A prior request with the same Idempotency-Key had a different payload; use a new key or resend the identical payload."` **Example:** ```json { "type": "urn:freyt:protocol:error:idempotency-conflict", "title": "Idempotency Conflict", "status": 409, "detail": "A prior request with the same Idempotency-Key had a different payload; use a new key or resend the identical payload.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBG", "idempotencyKey": "idem_01HYZABCDEF000000001" } ``` --- ### 8. `invalid-state-transition` — HTTP 422 **Type URI:** `urn:freyt:protocol:error:invalid-state-transition` **Title:** `Invalid State Transition` **Status:** 422 **Description:** The requested command is not a legal transition from the resource's current state. Consult the state-transition tables in `specification/state-machines/` for legal transitions. **Extension Fields:** | Field | Type | Description | |--------------------|--------|-------------------------------------------------------| | `resourceType` | string | The resource type (e.g. `CapacityOffer`). | | `resourceId` | string | The resource ID. | | `currentState` | string | The resource's current state. | | `requestedCommand` | string | The command that was attempted. | | `legalCommands` | array | Commands that are legal from the current state. | **Detail Format:** `"Command '{requestedCommand}' is not valid for a {resourceType} in state '{currentState}'."` **Example:** ```json { "type": "urn:freyt:protocol:error:invalid-state-transition", "title": "Invalid State Transition", "status": 422, "detail": "Command 'publish' is not valid for a CapacityOffer in state 'withdrawn'.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBH", "resourceType": "CapacityOffer", "resourceId": "co_01HYZ000000000000010", "currentState": "withdrawn", "requestedCommand": "publish", "legalCommands": [] } ``` --- ### 9. `resource-expired` — HTTP 422 **Type URI:** `urn:freyt:protocol:error:resource-expired` **Title:** `Resource Expired` **Status:** 422 **Description:** The resource reached its `validUntil` deadline and was automatically transitioned to an `expired` terminal state. No further commands are accepted. **Extension Fields:** | Field | Type | Description | |----------------|----------------|--------------------------------------------------| | `resourceType` | string | The resource type (e.g. `LoadRequest`). | | `resourceId` | string | The resource ID. | | `expiredAt` | string (dt) | ISO 8601 UTC timestamp when the resource expired. | **Detail Format:** `"The {resourceType} expired at {expiredAt} and no further commands are accepted."` **Example:** ```json { "type": "urn:freyt:protocol:error:resource-expired", "title": "Resource Expired", "status": 422, "detail": "The LoadRequest expired at 2024-09-01T12:00:00Z and no further commands are accepted.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBI", "resourceType": "LoadRequest", "resourceId": "lr_01HYZ000000000000020", "expiredAt": "2024-09-01T12:00:00Z" } ``` --- ### 10. `incompatible-decision-policy` — HTTP 422 **Type URI:** `urn:freyt:protocol:error:incompatible-decision-policy` **Title:** `Incompatible Decision Policy` **Status:** 422 **Description:** A `MatchDecision` was submitted but the decision type or terms are incompatible with the governing `MatchProposal`'s policy constraints. For example, submitting a `COUNTER` decision on a proposal that requires `ACCEPT_OR_REJECT` only, or a counter with terms outside the permitted range. **Extension Fields:** | Field | Type | Description | |------------------|--------|----------------------------------------------------------| | `decisionType` | string | The decision type that was submitted. | | `allowedTypes` | array | Decision types permitted for this proposal. | | `policyRef` | string | Identifier of the governing policy constraint. | **Detail Format:** `"Decision type '{decisionType}' is not permitted by the MatchProposal policy."` **Example:** ```json { "type": "urn:freyt:protocol:error:incompatible-decision-policy", "title": "Incompatible Decision Policy", "status": 422, "detail": "Decision type 'COUNTER' is not permitted by the MatchProposal policy.", "correlationId": "req_01HYZ4K2C3JVNSQ4WXPBJ", "decisionType": "COUNTER", "allowedTypes": ["ACCEPT", "REJECT"], "policyRef": "policy_accept_or_reject_only" } ``` --- ## Summary Table | Code | HTTP | Description | |------------------------------|------|----------------------------------------------------------------| | `validation-error` | 400 | Request body field validation failure | | `authentication-required` | 401 | Missing or invalid bearer token | | `authorization-denied` | 403 | Valid token lacks required scope or organization permission | | `resource-not-found` | 404 | Resource absent or hidden by existence-leakage policy | | `relationship-inactive` | 409 | No active PartnerRelationship between the organizations | | `stale-resource-version` | 409 | If-Match version mismatch; fetch latest and retry | | `idempotency-conflict` | 409 | Same Idempotency-Key reused with a different payload | | `invalid-state-transition` | 422 | Command not legal from the resource's current state | | `resource-expired` | 422 | Resource reached its validUntil deadline; terminal | | `incompatible-decision-policy` | 422 | MatchDecision type violates proposal policy constraints | --- ## Integration Points - The base schema `specification/schemas/problem-detail.schema.json` defines `type`, `title`, `status`, `detail`, `instance`, `correlationId`, and the `errors` array. - All `4xx`/`5xx` responses in `specification/openapi/openapi.yaml` reference `../schemas/problem-detail.schema.json` via `$ref`. - Extension fields documented above are additional properties not restricted by the base schema (`additionalProperties` is unset, so they pass validation). - Implementors MUST set `Content-Type: application/problem+json` on all error responses. - `correlationId` SHOULD be present on every error response to enable distributed tracing and support ticket lookup. --- # OpenFreyt Protocol — Normative Glossary Source: specification/glossary.md **Status:** Normative **Version:** 1.0.0-draft **Locked by:** M001-S01-T01 This glossary is the single authoritative vocabulary for all OpenFreyt Protocol (PCX) specification documents, schemas, conformance tests, and implementations. Terms defined here **MUST** be used exactly as specified. Where a term has a restricted meaning within PCX that differs from everyday usage, the PCX definition governs. --- ## 1. Locked Vocabulary Decisions The following four decisions were open questions in PROTOCOL.md §15. They are now permanently locked; downstream slices MAY NOT reopen them without a new ADR and milestone-level re-planning. ### 1.1 Decision A — Vehicle / Body-Type Vocabulary **Resolution:** Closed enum drawn from the **UNECE Recommendation 28** subset, with a namespaced extension escape hatch. **Canonical enum members:** | Member | Description | | --- | --- | | `tautliner` | Curtain-sided trailer with taut curtain system | | `curtainsider` | Curtain-sided trailer (general) | | `box` | Rigid box body (dry) | | `refrigerated` | Temperature-controlled enclosure | | `flatbed` | Flat platform without sides | | `tanker` | Liquid or gas bulk container | | `bulk` | Open-top bulk commodity carrier | | `heavy-lift` | Specialized heavy / over-dimensional equipment | | `low-loader` | Low-bed trailer for oversized cargo | | `swap-body` | Interchangeable swap-body unit (BIC/EN 284) | **Extension escape hatch:** any value prefixed `extension:` (e.g. `extension:walking-floor`) is valid and **MUST NOT** be rejected by conformant implementations. Schema validators **MUST** accept the pattern `^extension:[a-z0-9][a-z0-9-]*$`. Proprietary extensions are broker-scoped and carry no cross-broker interoperability guarantee. ### 1.2 Decision B — Area Representation **Resolution:** GeoJSON RFC 7946 geometry object — either a `Point` (with an added `radiusKm` property) or a `Polygon` — plus an optional `adminRegionCode` field for administrative region lookup. **Canonical area object fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | `geometry` | GeoJSON `Point` or `Polygon` | yes | RFC 7946 geometry object | | `radiusKm` | `number` (positive) | only when geometry is `Point` | Radius in kilometres around the point | | `adminRegionCode` | `string` | no | ISO 3166-1 alpha-2 country code, optionally suffixed with ISO 3166-2 subdivision code (e.g. `DE`, `DE-BY`) | A `Polygon` geometry **MUST** follow RFC 7946 §3.1.6 (exterior ring counter-clockwise, holes clockwise; first and last positions identical). `radiusKm` **MUST** be omitted or null when geometry is `Polygon`. ### 1.3 Decision C — Organization Identity **Resolution:** Organization identity is an **opaque, broker-scoped string** (`organizationId`). An optional structured `legalIdentifier` object provides cross-system linkage without coupling the protocol to a single registry. **`organizationId`:** opaque string, max 255 characters, broker-assigned. Uniqueness is scoped to the issuing broker service. **`legalIdentifier` object fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `"GLN"` \| `"EORI"` \| `"DUNS"` \| `"custom"` | yes | Legal identifier namespace | | `value` | `string` | yes | Identifier value within the namespace | `type: "custom"` is reserved for national or sector-specific registries. When `type` is `"custom"` a `customTypeLabel` string field **SHOULD** be provided. ### 1.4 Decision D — Decision Policy **Resolution:** Match-decision behaviour is governed by a `decisionPolicy` enum. The default is `BILATERAL_REQUIRED`. | Value | Semantics | | --- | --- | | `BILATERAL_REQUIRED` | A `MatchProposal` reaches `accepted` only when **both** the capacity owner and the load owner have accepted. Default. | | `INITIATOR_ONLY` | Only the broker service or the party that created the `MatchProposal` must accept; the other party's decision is advisory only. | | `ACCEPTOR_ONLY` | Only the non-initiating party must explicitly accept; the initiating party's acceptance is implicit on proposal creation. | `decisionPolicy` is a property of `MatchProposal`. Its value **MUST NOT** change after the proposal is created. --- ## 2. Common Envelope Every PCX resource carries the following **CommonEnvelope** fields. These fields are defined once here and inherited by all seven core resource types. | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | UUIDv7 string | yes | Globally unique resource identifier. **MUST** be a valid UUIDv7 (RFC 9562 §5.7). Monotonically increasing time prefix enables ordered pagination. | | `resourceVersion` | `integer` ≥ 1 | yes | Monotonic optimistic-concurrency counter. Incremented on every mutation. Consumers **MUST** treat a version gap > 1 as a missed-event signal. | | `ownerOrganizationId` | `string` | yes | Broker-scoped `organizationId` of the organization that owns this resource. Authorization checks compare this field. | | `createdAt` | ISO 8601 UTC datetime | yes | Instant the resource was first persisted. **MUST NOT** change after creation. | | `updatedAt` | ISO 8601 UTC datetime | yes | Instant of the last mutation. Equal to `createdAt` on first write. | | `externalReferences` | array of `ExternalReference` | no | Optional list of cross-system pointers (TMS order IDs, ERP references, etc.). | **`ExternalReference` object fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | `system` | `string` | yes | Identifier for the external system (e.g. `"sap-tm"`, `"samsara"`) | | `referenceId` | `string` | yes | The ID or key in that system | | `url` | `string` (URI) | no | Optional deep-link URL | --- ## 3. Core Resources PCX defines exactly **seven** core resources. All inherit CommonEnvelope. ### 3.1 PartnerRelationship A bilateral agreement between two organizations, established within a single broker service, that enables capacity and load data exchange. **Canonical additional fields:** | Field | Type | Description | | --- | --- | --- | | `initiatorOrganizationId` | `string` | Organization that sent the invitation | | `acceptorOrganizationId` | `string` | Organization that received the invitation | | `decisionPolicy` | `DecisionPolicy` | Default decision policy for matches within this relationship | | `permittedScopes` | `string[]` | Scopes explicitly granted within this relationship | | `invitedAt` | datetime | When the invitation was sent | | `activatedAt` | datetime \| null | When both parties accepted | **Lifecycle states:** | State | Meaning | | --- | --- | | `pending` | Invitation sent; acceptor has not yet responded | | `active` | Both parties have accepted; data exchange is permitted | | `suspended` | Temporarily disabled; no new matches may be created | | `terminated` | Permanently closed; all associated data becomes read-only | ### 3.2 CapacityOffer An offer of available transport capacity published by a carrier organization. **Canonical additional fields:** | Field | Type | Description | | --- | --- | --- | | `vehicleBodyType` | `VehicleBodyType` | Body type (see §1.1) | | `vehicleCount` | `integer` ≥ 1 | Number of vehicles available | | `originArea` | `Area` | Geographic origin area (see §1.2) | | `destinationArea` | `Area` | Geographic destination area | | `availableFrom` | datetime | Earliest loading readiness | | `availableUntil` | datetime | Latest departure window close | | `capacityWeightKg` | `number` | Max payload weight in kilograms | | `capacityVolumeM3` | `number` \| null | Max payload volume in cubic metres | | `publishedAt` | datetime \| null | When the offer was made visible | **Lifecycle states:** | State | Meaning | | --- | --- | | `draft` | Created but not visible to partners | | `published` | Visible to authorized partners; eligible for matching | | `partiallyMatched` | At least one match proposal exists; capacity partially allocated | | `matched` | Fully allocated; no further matches accepted | | `withdrawn` | Owner removed before expiry; no longer matchable | | `expired` | `availableUntil` passed without full allocation | | `cancelled` | Administratively cancelled by broker service | ### 3.3 LoadRequest A request for transport capacity published by a shipper or freight forwarder. **Canonical additional fields:** | Field | Type | Description | | --- | --- | --- | | `requiredBodyType` | `VehicleBodyType` | Required vehicle body type | | `originArea` | `Area` | Pick-up area | | `destinationArea` | `Area` | Delivery area | | `readyAt` | datetime | Cargo ready for pick-up | | `deliverBy` | datetime | Required delivery deadline | | `cargoWeightKg` | `number` | Cargo weight in kilograms | | `cargoVolumeM3` | `number` \| null | Cargo volume in cubic metres | | `hazmat` | `boolean` | Whether cargo is hazardous (ADR) | | `publishedAt` | datetime \| null | When the request was made visible | **Lifecycle states:** identical to CapacityOffer (`draft`, `published`, `partiallyMatched`, `matched`, `withdrawn`, `expired`, `cancelled`). ### 3.4 MatchProposal A broker-generated proposal linking one CapacityOffer to one LoadRequest. **Canonical additional fields:** | Field | Type | Description | | --- | --- | --- | | `capacityOfferId` | UUIDv7 | Referenced CapacityOffer | | `loadRequestId` | UUIDv7 | Referenced LoadRequest | | `proposedAt` | datetime | When the broker created the proposal | | `expiresAt` | datetime | Decision deadline | | `decisionPolicy` | `DecisionPolicy` | Policy governing acceptance (see §1.4) | | `capacityOwnerDecision` | `PartyDecision` \| null | Capacity owner's current decision | | `loadOwnerDecision` | `PartyDecision` \| null | Load owner's current decision | **Lifecycle states:** | State | Meaning | | --- | --- | | `open` | Proposal created; awaiting decisions per `decisionPolicy` | | `accepted` | Decision policy satisfied; MatchDecision record created | | `rejected` | At least one required party explicitly rejected | | `expired` | `expiresAt` passed before policy was satisfied | | `superseded` | A newer proposal for the same pair was created | | `withdrawn` | Broker or initiating party withdrew before decision | ### 3.5 MatchDecision The authoritative record that a `MatchProposal` reached `accepted` state. Created automatically when the proposal transitions to `accepted`. **Canonical additional fields:** | Field | Type | Description | | --- | --- | --- | | `matchProposalId` | UUIDv7 | The accepted MatchProposal | | `capacityOfferId` | UUIDv7 | Denormalized for query efficiency | | `loadRequestId` | UUIDv7 | Denormalized for query efficiency | | `decidedAt` | datetime | Instant the policy was satisfied | | `executionReferenceId` | UUIDv7 \| null | Link to ExecutionReference when created | **Lifecycle states:** | State | Meaning | | --- | --- | | `active` | Decision stands; execution may proceed | | `cancelled` | Decision voided (mutual cancellation or compliance override) | | `disputed` | Under formal dispute resolution | ### 3.6 ExecutionReference A lightweight operational record that tracks real-world execution of a matched transport. Created after `MatchDecision` reaches `active`. **Canonical additional fields:** | Field | Type | Description | | --- | --- | --- | | `matchDecisionId` | UUIDv7 | Source MatchDecision | | `scheduledPickupAt` | datetime | Planned pick-up time | | `scheduledDeliveryAt` | datetime | Planned delivery time | | `carrierReference` | `string` \| null | Carrier's own shipment reference | **Lifecycle states:** | State | Meaning | | --- | --- | | `planned` | Not yet started; no driver/vehicle assigned | | `inProgress` | Pick-up confirmed; shipment moving | | `completed` | Delivery confirmed | | `cancelled` | Transport cancelled after execution started | | `disputed` | Execution facts under dispute | ### 3.7 ExecutionOutcome The final verified record of transport completion, supporting audit, payment, and compliance review. **Canonical additional fields:** | Field | Type | Description | | --- | --- | --- | | `executionReferenceId` | UUIDv7 | Source ExecutionReference | | `actualPickupAt` | datetime \| null | Actual pick-up time | | `actualDeliveryAt` | datetime \| null | Actual delivery time | | `proofOfDeliveryUrl` | string (URI) \| null | Link to POD document | | `complianceReviewStatus` | `ComplianceReviewStatus` | Status of compliance review | | `reviewedBy` | `string` \| null | `organizationId` of reviewer | | `reviewedAt` | datetime \| null | When compliance review was completed | **Lifecycle states:** | State | Meaning | | --- | --- | | `pending` | Execution complete; awaiting outcome submission | | `submitted` | Outcome submitted; pending compliance review | | `approved` | Compliance review passed | | `rejected` | Compliance review failed; resubmission required | | `disputed` | Outcome facts under formal dispute | --- ## 4. Authorization Scopes PCX defines exactly **nine** authorization scopes. All scopes are **broker-scoped**: a token issued by broker A grants no rights on broker B. | Scope | Resource(s) | Description | | --- | --- | --- | | `relationships:manage` | PartnerRelationship | Create, update, suspend, and terminate partner relationships | | `capacity:publish` | CapacityOffer | Create and mutate own CapacityOffers; withdraw or cancel | | `capacity:read` | CapacityOffer | Read CapacityOffers where authorized by a PartnerRelationship | | `loads:publish` | LoadRequest | Create and mutate own LoadRequests; withdraw or cancel | | `loads:read` | LoadRequest | Read LoadRequests where authorized by a PartnerRelationship | | `matches:read` | MatchProposal, MatchDecision | Read match proposals and decisions for own resources | | `matches:decide` | MatchProposal | Submit accept/reject decisions on MatchProposals | | `outcomes:write` | ExecutionReference, ExecutionOutcome | Create and update execution records | | `outcomes:review` | ExecutionOutcome | Perform compliance review on ExecutionOutcomes | --- ## 5. Roles | Role | Description | | --- | --- | | `BrokerService` | The platform operating a PCX broker node. Issues tokens, enforces authorization, owns relationship lifecycle. | | `OrganizationService` | A machine client (TMS, ERP integration) acting on behalf of an organization. Uses OAuth 2.0 client credentials. | | `OrganizationMember` | A human user affiliated with an organization. Uses OAuth 2.0 authorization-code flow. | | `ComplianceReviewer` | A human or service that performs `outcomes:review` operations. May be affiliated with the BrokerService. | --- ## 6. General Terms | Term | Definition | | --- | --- | | **PCX** | OpenFreyt Protocol — Partner Capacity Exchange. The name of this protocol. | | **Broker** | An entity that operates a PCX-conformant platform service connecting multiple transport organizations. | | **UUIDv7** | A UUID as defined in RFC 9562 §5.7. Encodes a 48-bit Unix millisecond timestamp prefix for monotonic ordering. | | **Invariant** | A condition that **MUST** hold at all times for the system to be in a valid state. Invariants are defined in `requirements/invariants.yaml`. | | **Idempotency key** | A caller-supplied string that allows safe retry of mutating requests without duplication. | | **Monotonic resourceVersion** | The `resourceVersion` integer on a resource. Each mutation MUST increment it by exactly 1. | | **Optimistic concurrency** | A write strategy where the caller supplies the expected `resourceVersion`; the server rejects with HTTP 409 if the current version differs. | | **Pre-match** | The lifecycle phase before a MatchDecision exists. Certain sensitive fields (e.g. exact price, carrier identity) are suppressed during pre-match per R-005. | | **Post-match** | The lifecycle phase after a MatchDecision is `active`. Full field disclosure is permitted to authorized parties. | | **404-vs-403 rule** | When a requester lacks authorization to know a resource exists, the server **MUST** return HTTP 404 (not 403) to prevent resource enumeration. Defined in R-001. | | **Field-level disclosure** | The controlled reveal of sensitive resource fields based on lifecycle phase and authorization scope. Governed by the authorization matrix. | | **Relationship-gated** | An operation or data access that requires an `active` PartnerRelationship between the requesting organization and the resource-owning organization. | | **PartyDecision** | An object recording one party's accept or reject decision on a MatchProposal, with `decision` (`"accept"` \| `"reject"`), `decidedAt`, and optional `note` fields. | | **ComplianceReviewStatus** | Enum: `pending` \| `approved` \| `rejected` \| `waived`. Used on ExecutionOutcome. | | **ADR** | Architecture Decision Record. Stored under `docs/adr/`. Normative; referenced by decision IDs in this glossary and the authorization matrix. | | **Conformant implementation** | A broker service that passes all PCX conformance tests defined in `conformance/`. | | **Extension scope** | Any value matching `^extension:[a-z0-9][a-z0-9-]*$` in an enum field. Conformant implementations **MUST** accept extension values without rejecting the message. | | **Stale event** | An event whose `resourceVersion` is lower than the last known version for that resource ID. Consumers **MUST** discard stale events without side effects. | | **Double commitment** | The race condition where two concurrent match operations claim the same capacity or load. Protected by optimistic concurrency and idempotency keys. | | **Audit trail** | The immutable append-only log of all state transitions for PCX resources. Required by compliance and non-repudiation requirements. | --- # Openapi Source: specification/openapi/openapi.yaml openapi: 3.1.0 info: title: OpenFreyt Protocol — Partner Capacity Exchange (PCX) summary: Interoperable contract for partner capacity discovery, explicit decision, and outcome evidence description: | The Partner Capacity Exchange (PCX) protocol provides the smallest interoperable contract required for mutually authorised logistics partners to discover compatible capacity and demand, make explicit decisions, and prove the operational outcome. ## Authentication All operations require a valid OAuth 2.0 bearer token. Two flows are supported: - **Client Credentials** (`OAuth2ClientCredentials`) — for machine clients (`OrganizationService` role), typically a TMS or ERP integration. - **Authorization Code with PKCE** (`OAuth2AuthCode`) — for interactive users (`OrganizationMember` role). Tokens must carry the scopes listed per operation. Broker-scoped authority applies: a token from Broker A grants no rights on Broker B. ## Error Responses All error responses use RFC 9457 Problem Details (`application/problem+json`) with protocol-specific `type` URIs of the form `urn:freyt:protocol:error:{code}`. The full catalogue is in `specification/errors/error-catalogue.md`. ## Idempotency All mutating operations accept an `Idempotency-Key` header. Repeated requests with the same key and payload return the original response. A key reused with a different payload returns HTTP 409 (`idempotency-conflict`). ## Optimistic Concurrency State-changing commands accept `If-Match` carrying the observed `resourceVersion`. A stale version returns HTTP 409 (`stale-resource-version`). ## 404 vs 403 Rule (R-001) When a caller lacks authorisation to know a resource exists, the server MUST respond with HTTP 404, not HTTP 403. HTTP 403 is returned only when the caller owns the resource but the specific operation is forbidden. version: 1.0.0-draft contact: name: OpenFreyt Protocol Governance url: https://openfreytprotocol.example license: name: Apache-2.0 url: https://www.apache.org/licenses/LICENSE-2.0 jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema servers: - url: https://broker.example.com/pcx/v1 description: Example PCX broker endpoint (replace with the actual broker URL) tags: - name: partner-relationships description: Bilateral authorisation between two organisations - name: capacity-offers description: Available vehicle or trip capacity offered by a carrier - name: load-requests description: Demand for transport capacity posted by a shipper - name: match-proposals description: Broker-generated proposals connecting compatible offers and requests - name: execution-references description: Links between accepted matches and external TMS/ERP execution records - name: execution-outcomes description: Append-only execution outcome and correction records paths: # ── Partner Relationships ────────────────────────────────────────────────── /partner-relationships: post: operationId: createPartnerRelationship summary: Create a partner relationship invitation description: | Initiates a bilateral partnership invitation. The requesting organisation becomes the `initiatorOrganizationId`. The relationship starts in `draft` state and transitions to `requested` on successful creation. The invited organisation must call `POST /partner-relationships/{id}/accept` to activate the relationship. **Required scope:** `relationships:manage` **Allowed roles:** OrganizationService, OrganizationMember, BrokerService tags: [partner-relationships] security: - OAuth2ClientCredentials: [relationships:manage] - OAuth2AuthCode: [relationships:manage] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePartnerRelationshipRequest' responses: '201': description: Partner relationship invitation created; state is `requested` headers: Location: description: URL of the created PartnerRelationship resource schema: type: string format: uri content: application/json: schema: $ref: '../schemas/partner-relationship.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '409': $ref: '#/components/responses/IdempotencyConflict' '422': $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' /partner-relationships/{id}/accept: post: operationId: acceptPartnerRelationship summary: Accept a partner relationship invitation description: | Transitions a `requested` PartnerRelationship to `active`. Only the organisation identified by `acceptingOrganizationId` may call this operation. The `If-Match` header SHOULD carry the observed `resourceVersion` to prevent stale-write conflicts. **Required scope:** `relationships:manage` **Allowed roles:** OrganizationService, OrganizationMember, BrokerService tags: [partner-relationships] security: - OAuth2ClientCredentials: [relationships:manage] - OAuth2AuthCode: [relationships:manage] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AcceptPartnerRelationshipRequest' responses: '200': description: Partner relationship accepted and now `active` content: application/json: schema: $ref: '../schemas/partner-relationship.schema.json' '202': description: Acceptance received; broker is completing asynchronous activation (e.g. compliance checks) content: application/json: schema: $ref: '#/components/schemas/AsyncAcceptedResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/InvalidStateTransition' '500': $ref: '#/components/responses/InternalServerError' /partner-relationships/{id}/suspend: post: operationId: suspendPartnerRelationship summary: Suspend an active partner relationship description: | Transitions an `active` PartnerRelationship to `suspended`. Either party (initiator or acceptor) may suspend. The relationship can be reactivated (`POST /…/reactivate`) or terminated from the `suspended` state. **Required scope:** `relationships:manage` **Allowed roles:** OrganizationService, OrganizationMember, BrokerService tags: [partner-relationships] security: - OAuth2ClientCredentials: [relationships:manage] - OAuth2AuthCode: [relationships:manage] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuspendPartnerRelationshipRequest' responses: '200': description: Partner relationship suspended content: application/json: schema: $ref: '../schemas/partner-relationship.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/InvalidStateTransition' '500': $ref: '#/components/responses/InternalServerError' # ── Capacity Offers ──────────────────────────────────────────────────────── /capacity-offers: post: operationId: createCapacityOffer summary: Create a capacity offer draft description: | Creates a new CapacityOffer in `draft` state. The requesting organisation becomes the `ownerOrganizationId`. The offer is not visible to partners until `POST /capacity-offers/{id}/publish` is called. **Required scope:** `capacity:publish` **Allowed roles:** OrganizationService, OrganizationMember tags: [capacity-offers] security: - OAuth2ClientCredentials: [capacity:publish] - OAuth2AuthCode: [capacity:publish] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateCapacityOfferRequest' responses: '201': description: Capacity offer draft created; state is `draft` headers: Location: description: URL of the created CapacityOffer resource schema: type: string format: uri content: application/json: schema: $ref: '../schemas/capacity-offer.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '409': $ref: '#/components/responses/IdempotencyConflict' '422': $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' /capacity-offers/{id}/publish: post: operationId: publishCapacityOffer summary: Publish a capacity offer to the partner network description: | Transitions a `draft` CapacityOffer to `published`, making it discoverable by authorised partner organisations that hold `capacity:read` scope and an active PartnerRelationship. The broker may trigger asynchronous matching after publishing; the response is 202 in that case. **Required scope:** `capacity:publish` **Ownership required:** token `sub` must equal `ownerOrganizationId` tags: [capacity-offers] security: - OAuth2ClientCredentials: [capacity:publish] - OAuth2AuthCode: [capacity:publish] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PublishCapacityOfferRequest' responses: '200': description: Capacity offer published synchronously content: application/json: schema: $ref: '../schemas/capacity-offer.schema.json' '202': description: Publish accepted; broker is running asynchronous matching content: application/json: schema: $ref: '#/components/schemas/AsyncAcceptedResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/InvalidStateTransition' '500': $ref: '#/components/responses/InternalServerError' /capacity-offers/{id}/withdraw: post: operationId: withdrawCapacityOffer summary: Withdraw a capacity offer description: | Transitions a `draft`, `published`, or `partiallyMatched` CapacityOffer to `withdrawn`, removing it from partner discovery. This transition is terminal: a withdrawn offer cannot be republished. **Required scope:** `capacity:publish` **Ownership required:** token `sub` must equal `ownerOrganizationId` tags: [capacity-offers] security: - OAuth2ClientCredentials: [capacity:publish] - OAuth2AuthCode: [capacity:publish] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WithdrawCapacityOfferRequest' responses: '200': description: Capacity offer withdrawn content: application/json: schema: $ref: '../schemas/capacity-offer.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/InvalidStateTransition' '500': $ref: '#/components/responses/InternalServerError' # ── Load Requests ────────────────────────────────────────────────────────── /load-requests: post: operationId: createLoadRequest summary: Create a load request draft description: | Creates a new LoadRequest in `draft` state. The requesting organisation becomes the `ownerOrganizationId`. The request is not visible to partners until `POST /load-requests/{id}/publish` is called. **Required scope:** `loads:publish` **Allowed roles:** OrganizationService, OrganizationMember tags: [load-requests] security: - OAuth2ClientCredentials: [loads:publish] - OAuth2AuthCode: [loads:publish] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateLoadRequestRequest' responses: '201': description: Load request draft created; state is `draft` headers: Location: description: URL of the created LoadRequest resource schema: type: string format: uri content: application/json: schema: $ref: '../schemas/load-request.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '409': $ref: '#/components/responses/IdempotencyConflict' '422': $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' /load-requests/{id}/publish: post: operationId: publishLoadRequest summary: Publish a load request to the partner network description: | Transitions a `draft` LoadRequest to `published`, making it discoverable by authorised partner organisations that hold `loads:read` scope and an active PartnerRelationship. The broker may trigger asynchronous matching after publishing; the response is 202 in that case. **Required scope:** `loads:publish` **Ownership required:** token `sub` must equal `ownerOrganizationId` tags: [load-requests] security: - OAuth2ClientCredentials: [loads:publish] - OAuth2AuthCode: [loads:publish] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PublishLoadRequestRequest' responses: '200': description: Load request published synchronously content: application/json: schema: $ref: '../schemas/load-request.schema.json' '202': description: Publish accepted; broker is running asynchronous matching content: application/json: schema: $ref: '#/components/schemas/AsyncAcceptedResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/InvalidStateTransition' '500': $ref: '#/components/responses/InternalServerError' /load-requests/{id}/withdraw: post: operationId: withdrawLoadRequest summary: Withdraw a load request description: | Transitions a `draft`, `published`, or `proposed` LoadRequest to `withdrawn`. This transition is terminal: a withdrawn request cannot be republished. **Required scope:** `loads:publish` **Ownership required:** token `sub` must equal `ownerOrganizationId` tags: [load-requests] security: - OAuth2ClientCredentials: [loads:publish] - OAuth2AuthCode: [loads:publish] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WithdrawLoadRequestRequest' responses: '200': description: Load request withdrawn content: application/json: schema: $ref: '../schemas/load-request.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/InvalidStateTransition' '500': $ref: '#/components/responses/InternalServerError' # ── Match Proposals ──────────────────────────────────────────────────────── /match-proposals: get: operationId: listMatchProposals summary: List match proposals for the authenticated organisation description: | Returns match proposals where the authenticated organisation is a party (either the capacity-offer owner or the load-request owner). Results are filtered per the `matches:read` authorisation rules; proposals where the caller has no party membership are omitted (R-001). **Required scope:** `matches:read` tags: [match-proposals] security: - OAuth2ClientCredentials: [matches:read] - OAuth2AuthCode: [matches:read] parameters: - $ref: '#/components/parameters/PageCursor' - $ref: '#/components/parameters/PageLimit' - name: status in: query required: false description: Filter by proposal status schema: type: string enum: - open - accepted - rejected - expired - superseded - withdrawn - name: capacityOfferId in: query required: false description: Filter to proposals that reference this CapacityOffer schema: type: string format: uuid - name: loadRequestId in: query required: false description: Filter to proposals that reference this LoadRequest schema: type: string format: uuid responses: '200': description: Paginated list of match proposals content: application/json: schema: $ref: '#/components/schemas/MatchProposalListResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '500': $ref: '#/components/responses/InternalServerError' /match-proposals/{id}/decisions: post: operationId: recordMatchDecision summary: Record a match decision (accept, reject, or withdraw) description: | Appends an immutable MatchDecision to an `open` MatchProposal. The decision is made by the authenticated organisation, which must be a party to the proposal (capacity-offer owner or load-request owner). **Decision policy enforcement:** | Policy | Required before `accepted` | |---|---| | `BILATERAL_REQUIRED` | Both parties must submit `accept` | | `INITIATOR_ONLY` | Initiating organisation only | | `ACCEPTOR_ONLY` | Non-initiating organisation only | **Atomicity:** a concurrent `accept` and `withdraw` are resolved atomically by the broker; a committed match cannot be undone by a delayed withdrawal. A stale `observedProposalVersion` returns HTTP 409 (`stale-resource-version`). **Required scope:** `matches:decide` tags: [match-proposals] security: - OAuth2ClientCredentials: [matches:decide] - OAuth2AuthCode: [matches:decide] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RecordMatchDecisionRequest' responses: '201': description: Match decision recorded; decision is immutable headers: Location: description: URL of the created MatchDecision resource schema: type: string format: uri content: application/json: schema: $ref: '../schemas/match-decision.schema.json' '202': description: Decision recorded; broker is resolving the final proposal state asynchronously content: application/json: schema: $ref: '#/components/schemas/AsyncAcceptedResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' # ── Execution References ─────────────────────────────────────────────────── /execution-references: post: operationId: createExecutionReference summary: Create an execution reference linking a match to external execution records description: | Creates an ExecutionReference that links an accepted MatchProposal to external TMS/ERP records without copying operational detail into the protocol. Transitions the reference from implied `created` state to `linked`. **Required scope:** `outcomes:write` **Precondition:** an accepted MatchDecision must exist for the referenced `matchProposalId`. tags: [execution-references] security: - OAuth2ClientCredentials: [outcomes:write] - OAuth2AuthCode: [outcomes:write] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateExecutionReferenceRequest' responses: '201': description: Execution reference created; state is `linked` headers: Location: description: URL of the created ExecutionReference resource schema: type: string format: uri content: application/json: schema: $ref: '../schemas/execution-reference.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' # ── Execution Outcomes ───────────────────────────────────────────────────── /execution-outcomes: post: operationId: createExecutionOutcome summary: Submit an execution outcome record description: | Records whether and how an accepted exchange was executed. Execution outcomes are append-only. Corrections are submitted via `POST /execution-outcomes/{id}/corrections`, which creates a new record pointing back to this one. **Required scope:** `outcomes:write` **Precondition:** an active ExecutionReference must exist for the referenced match. tags: [execution-outcomes] security: - OAuth2ClientCredentials: [outcomes:write] - OAuth2AuthCode: [outcomes:write] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateExecutionOutcomeRequest' responses: '201': description: Execution outcome submitted; state is `submitted` headers: Location: description: URL of the created ExecutionOutcome resource schema: type: string format: uri content: application/json: schema: $ref: '../schemas/execution-outcome.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' /execution-outcomes/{id}/corrections: post: operationId: correctExecutionOutcome summary: Submit a correction to an execution outcome description: | Appends a new ExecutionOutcome record that supersedes the identified record. The correction carries a `supersedes` pointer to the prior record and a mandatory `correctionReason`. The original record is never overwritten; corrections form an acyclic chain. The single-head invariant is enforced: if a concurrent correction targets the same `id`, the one that wins sets `resourceVersion`; the other receives HTTP 409 (`stale-resource-version`). **Required scope:** `outcomes:write` tags: [execution-outcomes] security: - OAuth2ClientCredentials: [outcomes:write] - OAuth2AuthCode: [outcomes:write] parameters: - $ref: '#/components/parameters/ResourceId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CorrectExecutionOutcomeRequest' responses: '201': description: Correction submitted; new ExecutionOutcome record is the head of the chain headers: Location: description: URL of the newly created correction ExecutionOutcome record schema: type: string format: uri content: application/json: schema: $ref: '../schemas/execution-outcome.schema.json' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/AuthenticationRequired' '403': $ref: '#/components/responses/AuthorizationDenied' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/ConflictError' '422': $ref: '#/components/responses/UnprocessableEntity' '500': $ref: '#/components/responses/InternalServerError' components: # ── Shared Parameters ────────────────────────────────────────────────────── parameters: ResourceId: name: id in: path required: true description: UUIDv7 identifier of the target resource schema: type: string format: uuid IdempotencyKey: name: Idempotency-Key in: header required: false description: | Client-generated key scoped to the authenticated organisation and operation. Repeated requests with the same key and identical payload return the original response. A key reused with a different payload returns HTTP 409 (`idempotency-conflict`). schema: type: string maxLength: 255 IfMatch: name: If-Match in: header required: false description: | Observed `resourceVersion` of the target resource for optimistic concurrency control. A stale version returns HTTP 409 (`stale-resource-version`). schema: type: string PageCursor: name: cursor in: query required: false description: Opaque pagination cursor from a prior response `nextCursor` field schema: type: string PageLimit: name: limit in: query required: false description: Maximum number of items to return per page (1–100) schema: type: integer minimum: 1 maximum: 100 default: 20 # ── Command Request Schemas ──────────────────────────────────────────────── schemas: # ─ Partner Relationships ─ CreatePartnerRelationshipRequest: type: object description: Request body for creating a partner relationship invitation required: - acceptingOrganizationId - permittedScopes - visibilityPolicy - effectiveFrom properties: acceptingOrganizationId: type: string description: Broker-scoped identifier of the invited partner organisation permittedScopes: type: array description: OAuth 2.0 scopes the initiator proposes to grant to the acceptor minItems: 1 items: type: string enum: - capacity:read - loads:read - matches:read - matches:decide - outcomes:review visibilityPolicy: type: string description: Governs which resources are visible to the partner organisation enum: [default, restricted, extended] effectiveFrom: type: string format: date-time description: RFC 3339 UTC timestamp when the relationship becomes effective effectiveUntil: type: string format: date-time description: Optional RFC 3339 UTC expiry; relationship auto-terminates at this time retentionPolicyRef: type: string description: Optional reference identifier for the applicable data-retention policy externalReferences: type: array items: $ref: '#/components/schemas/ExternalReference' AcceptPartnerRelationshipRequest: type: object description: Request body for accepting a pending partner relationship invitation required: - observedResourceVersion properties: observedResourceVersion: type: integer minimum: 1 description: Current `resourceVersion` of the relationship (optimistic lock) permittedScopes: type: array description: Scopes the acceptor grants back to the initiator; defaults to matching the initiator's proposed scopes items: type: string enum: - capacity:read - loads:read - matches:read - matches:decide - outcomes:review comment: type: string maxLength: 1024 description: Optional human-readable note from the accepting organisation SuspendPartnerRelationshipRequest: type: object description: Request body for suspending an active partner relationship required: - observedResourceVersion - reason properties: observedResourceVersion: type: integer minimum: 1 description: Current `resourceVersion` of the relationship (optimistic lock) reason: type: string maxLength: 1024 description: Human-readable reason for suspension (required for audit trail) reasonCode: type: string description: Machine-readable reason code enum: - operational - compliance - temporary-pause - policy-change # ─ Capacity Offers ─ CreateCapacityOfferRequest: type: object description: Request body for creating a capacity offer draft required: - availabilityWindow - originArea - destinationArea - vehicleProfile - availableCapacity - expiresAt - visibility properties: availabilityWindow: $ref: '#/components/schemas/TimeWindow' originArea: $ref: '#/components/schemas/GeoArea' destinationArea: $ref: '#/components/schemas/GeoArea' direction: type: string description: Optional direction qualifier when the areas do not imply a single direction enum: [outbound, inbound, bilateral] vehicleProfile: $ref: '../schemas/vehicle-spec.schema.json' availableCapacity: $ref: '#/components/schemas/CapacityDimensions' constraints: type: array description: Optional handling or cargo constraints (e.g. tail-lift, ADR, temperature) items: type: string maximumDetour: $ref: '#/components/schemas/DetourConstraint' expiresAt: type: string format: date-time description: RFC 3339 UTC timestamp when the offer auto-expires if not withdrawn earlier visibility: $ref: '#/components/schemas/VisibilitySpec' externalReferences: type: array items: $ref: '#/components/schemas/ExternalReference' PublishCapacityOfferRequest: type: object description: Request body for publishing a draft capacity offer required: - observedResourceVersion properties: observedResourceVersion: type: integer minimum: 1 description: Current `resourceVersion` of the offer (optimistic lock) WithdrawCapacityOfferRequest: type: object description: Request body for withdrawing a capacity offer required: - observedResourceVersion - reason properties: observedResourceVersion: type: integer minimum: 1 description: Current `resourceVersion` of the offer (optimistic lock) reason: type: string maxLength: 1024 description: Human-readable reason for withdrawal reasonCode: type: string description: Machine-readable reason code enum: - capacity-no-longer-available - scheduling-conflict - duplicate - policy - other # ─ Load Requests ─ CreateLoadRequestRequest: type: object description: Request body for creating a load request draft required: - pickupArea - pickupWindow - deliveryArea - deliveryWindow - requiredCapacity - vehicleRequirements - goodsProfile - expiresAt - visibility properties: pickupArea: $ref: '#/components/schemas/GeoArea' pickupWindow: $ref: '#/components/schemas/TimeWindow' deliveryArea: $ref: '#/components/schemas/GeoArea' deliveryWindow: $ref: '#/components/schemas/TimeWindow' requiredCapacity: $ref: '#/components/schemas/CapacityDimensions' vehicleRequirements: type: array description: Required vehicle body types (e.g. curtain-side, refrigerated) items: type: string handlingRequirements: type: array description: Optional handling requirements (e.g. tail-lift, stackable, fragile) items: type: string goodsProfile: $ref: '#/components/schemas/GoodsProfile' expiresAt: type: string format: date-time description: RFC 3339 UTC timestamp when the request auto-expires visibility: $ref: '#/components/schemas/VisibilitySpec' externalReferences: type: array items: $ref: '#/components/schemas/ExternalReference' PublishLoadRequestRequest: type: object description: Request body for publishing a draft load request required: - observedResourceVersion properties: observedResourceVersion: type: integer minimum: 1 description: Current `resourceVersion` of the request (optimistic lock) WithdrawLoadRequestRequest: type: object description: Request body for withdrawing a load request required: - observedResourceVersion - reason properties: observedResourceVersion: type: integer minimum: 1 description: Current `resourceVersion` of the request (optimistic lock) reason: type: string maxLength: 1024 description: Human-readable reason for withdrawal reasonCode: type: string description: Machine-readable reason code enum: - demand-fulfilled - duplicate - scheduling-conflict - policy - other # ─ Match Proposals ─ RecordMatchDecisionRequest: type: object description: Request body for recording a match decision required: - decision - observedProposalVersion properties: decision: type: string description: The decision being recorded by the authenticated organisation enum: [accept, reject, withdraw] observedProposalVersion: type: integer minimum: 1 description: Current `resourceVersion` of the proposal; stale version returns HTTP 409 reasonCode: type: string description: Machine-readable reason code; RECOMMENDED for `reject` and `withdraw` maxLength: 64 comment: type: string description: Optional human-readable comment; access-controlled per authorisation matrix §3.6 maxLength: 4096 decidedBy: type: string description: Broker-scoped actor reference (user ID or service account) recording the decision MatchProposalListResponse: type: object description: Paginated list of match proposals required: - items - total properties: items: type: array items: $ref: '../schemas/match-proposal.schema.json' total: type: integer minimum: 0 description: Total number of proposals matching the applied filters nextCursor: type: string description: Opaque cursor for the next page; absent when this is the last page # ─ Execution References ─ CreateExecutionReferenceRequest: type: object description: Request body for creating an execution reference required: - matchProposalId - capacityOfferId - loadRequestId - externalReferences properties: matchProposalId: type: string format: uuid description: ID of the accepted MatchProposal being referenced capacityOfferId: type: string format: uuid description: ID of the CapacityOffer within the accepted match loadRequestId: type: string format: uuid description: ID of the LoadRequest within the accepted match externalReferences: type: array description: External TMS/ERP execution record references (at least one required) minItems: 1 items: $ref: '#/components/schemas/ExternalReference' plannedTimeRange: $ref: '#/components/schemas/TimeWindow' participatingOrganizations: type: array description: Broker-scoped organisation IDs of all parties to the execution minItems: 1 items: type: string # ─ Execution Outcomes ─ CreateExecutionOutcomeRequest: type: object description: Request body for submitting an execution outcome required: - executionReferenceId - status - deduplicationKey properties: executionReferenceId: type: string format: uuid description: ID of the ExecutionReference this outcome reports against status: type: string description: Outcome execution status enum: - planned - inProgress - completed - cancelled - disputed plannedTimeRange: $ref: '#/components/schemas/TimeWindow' actualTimeRange: $ref: '#/components/schemas/TimeWindow' participantConfirmations: type: array description: Confirmation records from participating organisations items: $ref: '#/components/schemas/ParticipantConfirmation' externalExecutionReferences: type: array description: Additional external execution record identifiers items: $ref: '#/components/schemas/ExternalReference' operationalDeviations: type: string maxLength: 4096 description: Human-readable description of operational deviations or exceptions capacityUtilization: $ref: '#/components/schemas/CapacityDimensions' deduplicationKey: type: string description: Client-generated key to prevent duplicate outcome submissions maxLength: 255 CorrectExecutionOutcomeRequest: type: object description: Request body for correcting an execution outcome; creates a new immutable record required: - observedResourceVersion - correctionReason - correctedFields properties: observedResourceVersion: type: integer minimum: 1 description: Current `resourceVersion` of the outcome being corrected (optimistic lock) correctionReason: type: string maxLength: 4096 description: Human-readable reason for the correction (mandatory for audit trail) correctionReasonCode: type: string description: Machine-readable correction reason code enum: - data-entry-error - late-information - status-update - operational-deviation - other correctedFields: type: object description: | Partial patch — only the fields being corrected. Applied by the broker on top of the prior head record to form the new canonical state. properties: status: type: string enum: - planned - inProgress - completed - cancelled - disputed actualTimeRange: $ref: '#/components/schemas/TimeWindow' operationalDeviations: type: string maxLength: 4096 capacityUtilization: $ref: '#/components/schemas/CapacityDimensions' # ─ Shared sub-schemas ─ AsyncAcceptedResponse: type: object description: Acknowledgement returned on HTTP 202 for commands that the broker processes asynchronously required: - correlationId - statusUrl properties: correlationId: type: string description: Broker-assigned identifier for tracing the asynchronous operation statusUrl: type: string format: uri description: URL to poll for the eventual resource state; returns the resource when processing completes estimatedCompletionMs: type: integer minimum: 0 description: Broker's estimate of processing duration in milliseconds (informational) TimeWindow: type: object description: An inclusive time window defined by start and end RFC 3339 timestamps required: - start - end properties: start: type: string format: date-time description: RFC 3339 UTC start of the window end: type: string format: date-time description: RFC 3339 UTC end of the window GeoArea: type: object description: | A geographic area represented by at least one of: UN/LOCODE, ISO 3166-2 administrative region code, or GeoJSON geometry. properties: locode: type: string description: UN/LOCODE (e.g. NLRTM for Rotterdam) pattern: '^[A-Z]{2}[A-Z2-9]{3}$' adminRegionCode: type: string description: ISO 3166-2 administrative region code (e.g. NL-ZH) pattern: '^[A-Z]{2}-[A-Z0-9]{1,3}$' geoJson: type: object description: GeoJSON RFC 7946 Polygon or MultiPolygon required: - type - coordinates properties: type: type: string enum: [Polygon, MultiPolygon] coordinates: type: array CapacityDimensions: type: object description: | Independently-optional capacity dimensions. Omitted dimensions carry no implied value; consumers MUST NOT infer a zero or maximum value. properties: weight: $ref: '#/components/schemas/Measure' volume: $ref: '#/components/schemas/Measure' loadingMetres: $ref: '#/components/schemas/Measure' palletSpaces: type: integer minimum: 0 description: Number of euro-pallet equivalent spaces Measure: type: object description: A non-negative numeric quantity paired with a UNECE Recommendation 20 unit code required: - value - unitCode properties: value: type: number minimum: 0 unitCode: type: string description: "UN/CEFACT Recommendation 20 unit code (e.g. KGM = kilograms, MTQ = cubic metres, MTR = metres)" DetourConstraint: type: object description: Maximum acceptable detour for this offer from the primary route properties: distanceKm: type: number minimum: 0 description: Maximum detour distance in kilometres durationMinutes: type: integer minimum: 0 description: Maximum detour duration in minutes GoodsProfile: type: object description: | Coarse, non-sensitive goods classification. Full shipper identity, consignee addresses, and detailed goods descriptions are excluded from the pre-match phase unless the visibility policy explicitly permits disclosure. required: - cargoType properties: cargoType: type: string description: Coarse cargo classification (e.g. general, hazardous, perishable, oversized) hazardousGoodsClass: type: string description: ADR/IMDG hazard class if applicable (e.g. "3", "6.1") temperatureControlled: type: boolean description: Whether temperature-controlled transport is required temperatureRangeCelsius: type: object description: Required temperature range when temperatureControlled is true required: [min, max] properties: min: type: number max: type: number stackable: type: boolean description: Whether the goods may be stacked VisibilitySpec: type: object description: Controls which partner organisations may discover this resource required: - mode properties: mode: type: string description: | - `all-partners`: visible to all orgs with an active relationship and the required scope - `explicit-list`: visible only to the listed partner organisation IDs - `relationship-group`: visible to partners in the named relationship group enum: - all-partners - explicit-list - relationship-group partnerOrganizationIds: type: array description: Required when `mode` is `explicit-list` items: type: string relationshipGroupRef: type: string description: Required when `mode` is `relationship-group` ExternalReference: type: object description: Namespaced reference to a record in an external system required: - system - type - value properties: system: type: string description: System namespace (e.g. carrier-tms, shipper-erp) type: type: string description: Record type within the named system (e.g. trip, order, shipment) value: type: string description: Record identifier; not globally unique without `system` + `type` context ParticipantConfirmation: type: object description: Confirmation record from a participating organisation required: - organizationId - confirmedAt - confirmedBy properties: organizationId: type: string description: Broker-scoped organisation identifier confirmedAt: type: string format: date-time description: RFC 3339 UTC timestamp of the confirmation confirmedBy: type: string description: Broker-scoped actor reference (user ID or service account) comment: type: string maxLength: 1024 # ── Shared Error Responses ───────────────────────────────────────────────── responses: ValidationError: description: "400 — Request body or query parameters failed schema or semantic validation" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' example: type: "urn:freyt:protocol:error:validation-error" title: "Validation error" status: 400 detail: "Field 'effectiveFrom' must be a valid RFC 3339 timestamp." instance: "/partner-relationships" errors: - field: "/effectiveFrom" message: "Invalid date-time format; expected RFC 3339 (e.g. 2026-03-01T08:00:00Z)" code: "invalid-format" AuthenticationRequired: description: "401 — Missing, expired, or invalid bearer token" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' example: type: "urn:freyt:protocol:error:authentication-required" title: "Authentication required" status: 401 detail: "Bearer token is missing, expired, or was issued for a different audience." AuthorizationDenied: description: "403 — Token is valid but the operation is forbidden (returned only when the caller owns the resource; see R-001)" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' example: type: "urn:freyt:protocol:error:authorization-denied" title: "Authorization denied" status: 403 detail: "The required scope 'relationships:manage' is not present in the token." ResourceNotFound: description: "404 — Resource not found, or the caller is not authorised to know it exists (R-001)" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' example: type: "urn:freyt:protocol:error:resource-not-found" title: "Resource not found" status: 404 detail: "The requested resource does not exist, or you are not authorised to access it." ConflictError: description: "409 — Conflict; discriminate the exact condition by `type` in the problem body" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' examples: staleResourceVersion: summary: Stale resource version (optimistic concurrency) value: type: "urn:freyt:protocol:error:stale-resource-version" title: "Stale resource version" status: 409 detail: "The resourceVersion submitted is behind the current server version." currentVersion: 7 relationshipInactive: summary: Relationship precondition not met value: type: "urn:freyt:protocol:error:relationship-inactive" title: "Relationship inactive" status: 409 detail: "The operation requires an active PartnerRelationship which is currently suspended." idempotencyConflict: summary: Idempotency key reused with different payload value: type: "urn:freyt:protocol:error:idempotency-conflict" title: "Idempotency key conflict" status: 409 detail: "The supplied Idempotency-Key was already used with a different request payload." IdempotencyConflict: description: "409 — Idempotency-Key reused with a different payload" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' example: type: "urn:freyt:protocol:error:idempotency-conflict" title: "Idempotency key conflict" status: 409 detail: "The supplied Idempotency-Key was already used with a different request payload for this operation." InvalidStateTransition: description: "422 — The command cannot be applied in the resource's current state" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' example: type: "urn:freyt:protocol:error:invalid-state-transition" title: "Invalid state transition" status: 422 detail: "Cannot publish a capacity offer that is already withdrawn." currentState: "withdrawn" command: "publish" UnprocessableEntity: description: "422 — Semantically invalid request; discriminate by `type` in the problem body" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' examples: invalidStateTransition: summary: Invalid state transition value: type: "urn:freyt:protocol:error:invalid-state-transition" title: "Invalid state transition" status: 422 detail: "The 'publish' command cannot be applied to a resource in 'withdrawn' state." currentState: "withdrawn" command: "publish" resourceExpired: summary: Resource expired before the operation could complete value: type: "urn:freyt:protocol:error:resource-expired" title: "Resource expired" status: 422 detail: "The match proposal expired before a decision was recorded." incompatibleDecisionPolicy: summary: Decision policy not satisfied value: type: "urn:freyt:protocol:error:incompatible-decision-policy" title: "Incompatible decision policy" status: 422 detail: "BILATERAL_REQUIRED policy not satisfied: counterparty decision is still pending." InternalServerError: description: "500 — Unexpected server error; safe to retry with the same Idempotency-Key" content: application/problem+json: schema: $ref: '../schemas/problem-detail.schema.json' example: type: "urn:freyt:protocol:error:internal-server-error" title: "Internal server error" status: 500 detail: "An unexpected error occurred. Retry with the same Idempotency-Key is safe." # ── Security Schemes ─────────────────────────────────────────────────────── securitySchemes: OAuth2ClientCredentials: type: oauth2 description: | OAuth 2.0 Client Credentials flow for machine clients (`OrganizationService` role). Tokens are issued by the broker's authorisation server. All nine PCX scopes are available. flows: clientCredentials: tokenUrl: https://broker.example.com/oauth2/token scopes: relationships:manage: Create, accept, suspend, and terminate partner relationships capacity:publish: Create, publish, update, and withdraw capacity offers capacity:read: Read and search partner capacity offers (active PartnerRelationship required) loads:publish: Create, publish, update, and withdraw load requests loads:read: Read and search partner load requests (active PartnerRelationship required) matches:read: Read and list match proposals and decisions matches:decide: Submit accept, reject, or withdraw decisions on match proposals outcomes:write: Create execution references and submit or correct execution outcomes outcomes:review: Read and review execution outcomes (ComplianceReviewer role) OAuth2AuthCode: type: oauth2 description: | OAuth 2.0 Authorization Code flow with PKCE for interactive users (`OrganizationMember` role). Tokens are issued by the broker's authorisation server. All nine PCX scopes are available. flows: authorizationCode: authorizationUrl: https://broker.example.com/oauth2/authorize tokenUrl: https://broker.example.com/oauth2/token scopes: relationships:manage: Create, accept, suspend, and terminate partner relationships capacity:publish: Create, publish, update, and withdraw capacity offers capacity:read: Read and search partner capacity offers (active PartnerRelationship required) loads:publish: Create, publish, update, and withdraw load requests loads:read: Read and search partner load requests (active PartnerRelationship required) matches:read: Read and list match proposals and decisions matches:decide: Submit accept, reject, or withdraw decisions on match proposals outcomes:write: Create execution references and submit or correct execution outcomes outcomes:review: Read and review execution outcomes (ComplianceReviewer role) --- # Private Exchange Profile Source: specification/private-exchange-profile.md **Status:** Pre-alpha normative design **Applies to:** brokers, portals, transport management system connectors and conformance tooling This profile extends the Partner Capacity Exchange from discovery into tendering, bilateral commitment and operational handover between heterogeneous systems. ## 1. Scope and system boundaries The exchange coordinates mutually accepted logistics partners. It is authoritative for: - organization and connector registrations; - partner relationships and bilateral policy; - published capacity and demand; - proposals, tenders and decisions; - commitments and handover acknowledgement; - shared lifecycle events and audit history. A connected system remains authoritative for its local customers, orders, shipments, routes, vehicles, drivers, invoices, planning state and execution records. The protocol MUST NOT transfer a vendor-native object as its canonical representation. It MUST transfer a bounded, vendor-neutral execution package. Each system creates or links its own local record and records that link as a namespaced external reference. ## 2. Exchange products ### 2.1 CapacityOffer A declaration of available vehicle movement or residual capacity. It supports discovery and reservation but is not an assignment. ### 2.2 LoadRequest A declaration of freight requiring compatible capacity. It contains only the information permitted for its disclosure phase. ### 2.3 MatchProposal An explainable, non-binding compatibility result. It MUST identify evaluated, satisfied, failed and unevaluated constraints. It MUST NOT itself reserve capacity or create an execution obligation. ### 2.4 TransportTender A request for a partner to execute a specified movement. A tender freezes the decision-relevant terms presented to the recipient and references the originating proposal or bilateral workflow where applicable. Minimum information includes: - issuer and intended executing organization; - expiry and decision policy; - movement scope: full transport, route leg, collection or delivery; - coarse or exact operational details according to disclosure policy; - capacity, cargo and equipment requirements; - commercial terms or a stable external terms reference; - amendment and cancellation policy; - source external references. A counterproposal creates a new immutable tender revision linked to the preceding revision. It MUST NOT silently modify terms already presented for decision. ### 2.5 TransportCommitment Immutable proof that all decisions required by the tender policy were accepted before expiry or valid withdrawal. It records: - the accepted tender revision; - required decisions and actors; - accepted-at timestamps; - a deterministic digest of the accepted decision-relevant payload; - the bilateral policy version in force; - the resulting capacity reservation or consumption. A commitment is not itself proof that the executing system imported the work. ### 2.6 ExecutionHandover The disclosure-controlled operational package required for the executing organization to create or link its local work record. Minimum structure: ```yaml id: resourceVersion: commitmentId: sourceOrganizationId: executingOrganizationId: externalReferences: [] movement: type: full-transport | route-leg | collection | delivery stops: - sequence: function: pickup | delivery | waypoint location: timeWindow: references: [] instructions: contact: cargo: description: packages: [] grossWeight: volume: loadingMetres: palletSpaces: temperatureRange: dangerousGoods: handlingRequirements: [] equipment: bodyType: capabilities: [] documents: [] commercialTermsReference: disclosurePolicy: lifecyclePolicy: acceptedSnapshotHash: ``` Exact addresses, contacts, customer identity, documents, price and sensitive cargo details MUST be released only when the relationship and resource disclosure policies permit them. ### 2.7 HandoverAcknowledgement The executing connector records one of: - `imported`: a local execution record was created; - `linked`: an existing local execution record was linked; - `rejected`: the package could not be accepted, with a stable reason code; - `quarantined`: automatic import was unsafe and requires authorized review. An acknowledgement includes destination external references. Duplicate handover delivery MUST resolve to the same local record and acknowledgement. ### 2.8 ExecutionUpdate and ExecutionOutcome Execution updates communicate shared milestones and exceptions without replacing the local system's detailed state. Supported baseline milestones are: ```text assigned at-pickup picked-up in-transit delayed at-delivery delivered failed cancelled disputed ``` Corrections append or supersede prior updates. They never erase accepted decisions or historical evidence. ## 3. Lifecycle ```text capacity/load publication -> match proposal -> tender draft -> tender issued -> viewed -> accepted | rejected | expired | withdrawn | countered -> commitment created -> handover offered -> imported | linked | rejected | quarantined -> assigned -> execution updates -> completed | cancelled | failed | disputed -> corrected outcome when necessary ``` Commercial acceptance and technical handover acknowledgement MUST remain separate. An amendment after commitment MUST: 1. identify the commitment and prior revision; 2. state changed fields and reason; 3. preserve the previously accepted snapshot; 4. require renewed acceptance when a decision-relevant term changes; 5. leave the existing commitment effective until policy defines replacement or cancellation. Cancellation MUST be an explicit command and event. Deleting a resource does not cancel an operational commitment. ## 4. Authentication profile ### 4.1 Machine clients Connectors use OAuth 2.0 Client Credentials. Production deployments SHOULD authenticate clients with `private_key_jwt` or mutual TLS rather than a long-lived shared secret. An access token MUST contain or resolve to: - issuer; - subject identifying the connector; - exact broker API audience; - issued-at and expiry; - unique token identifier; - client identifier; - owning organization identifier; - granted scopes. The broker MUST bind a connector registration to exactly one organization in one environment. Payload fields cannot override that binding. The broker SHOULD derive resource ownership from authenticated context. Credentials and signing keys MUST support rotation, revocation and environment separation. Machine clients SHOULD use short-lived access tokens. ### 4.2 Interactive users Interactive users authenticate with OpenID Connect Authorization Code and PKCE. The resulting authorization context MUST identify the user, organization membership and permitted roles. High-impact actions such as commitment acceptance MAY require step-up authentication according to exchange policy. A connected system's internal session token MUST NOT be accepted as an exchange credential unless a specified token-exchange or federation profile validates issuer, audience, organization and actor semantics. ## 5. Authorization profile Every protected operation evaluates: 1. token validity and audience; 2. active connector or user registration; 3. active organization; 4. resource ownership or participation; 5. active partner relationship; 6. client scope; 7. bilateral relationship policy; 8. resource visibility; 9. field disclosure phase; 10. actor role; 11. lifecycle preconditions; 12. current resource version and expiry. API scopes describe what a client may call. Relationship policy describes what two organizations permit between each other. Resource visibility describes who may observe one resource. Passing one layer MUST NOT bypass another. Recommended additional scopes are: ```text tenders:publish tenders:read tenders:decide commitments:read handovers:write handovers:read handovers:acknowledge execution:update connectors:manage ``` The exchange SHOULD conceal resource existence with `404` where `403` would disclose protected partner activity. ## 6. Organization, connector and bilateral trust An organization registration contains a broker-scoped identifier, verified legal identity, status, administrators, accepted terms, security contacts and registered connectors. Legal identifiers are attributes; they do not replace the broker-scoped identifier. A connector registration contains: - immutable client identifier; - owning organization; - software product and adapter version; - supported protocol profiles and versions; - authentication and event-verification keys; - permitted scopes; - environment and callback endpoints; - conformance status; - health and revocation state. A partner relationship policy SHOULD define: - permitted exchange products and actions; - geographic or business restrictions; - visibility and disclosure stages; - decision policy; - onward-subcontracting permission; - commercial-field visibility; - update obligations; - document permissions; - amendment and cancellation rules; - retention and dispute contacts; - effective and expiry dates. Suspension prevents new exchange activity. It MUST NOT erase history or prevent the minimum authorized processing required to finish, cancel or audit existing commitments. ## 7. Connector contract A conformant connector MUST: - map local records to canonical resources without inventing required values; - preserve source and destination external references; - bind all processing to its authenticated organization; - validate payloads before publishing or importing; - quarantine ambiguous or unsafe mappings; - process commands and events idempotently; - persist inbox, outbox and mapping state durably; - recover from duplicate, reordered and missed delivery; - reconcile authoritative resources from a cursor or version; - expose health, lag, mapping failures and dead letters; - redact sensitive values from logs. A connector SHOULD maintain the exchange resource ID and version, local system and resource type, local ID, last imported/exported versions, synchronization state and last stable error code. Direct writes to a connected system's internal database are non-conformant unless that system explicitly documents the database interface as a supported integration contract. ## 8. Idempotency, consistency and recovery State-changing commands MUST carry an idempotency key. Mutable-resource commands MUST assert the expected resource version. The implementation mechanism is not prescribed, but observable behavior MUST guarantee: - no commitment without all required acceptances; - no commitment after valid withdrawal or expiry; - no double commitment of exclusive capacity; - one idempotency key has one effect; - duplicate handovers do not create duplicate local work; - reordered events do not regress state; - amendments cannot silently replace accepted terms. Connectors SHOULD use a transactional outbox and durable inbox. Event delivery is at least once. Event notifications are not the only durable source: recipients MUST be able to retrieve and reconcile authoritative resources. An accepted tender whose destination import fails remains committed but not handed over. The failure MUST be observable and recoverable; it MUST NOT be reported as successful execution transfer. ## 9. Event security The event security mechanism will be fixed in the security specification before v0.1. It MUST provide: - producer and destination authentication; - payload integrity; - covered event identifier and timestamp; - replay resistance; - key identification, rotation and revocation; - retry, dead-letter inspection and authorized replay. A conformant profile may use mutual TLS with OAuth-protected retrieval, HTTP Message Signatures, or signed event payloads only after one method and its canonicalization rules are selected normatively. ## 10. Capability and version negotiation A connector capabilities endpoint MUST advertise: - protocol and profile versions; - supported resources and commands; - supported event delivery modes; - optional cargo, equipment and document capabilities; - maximum payload and document limits; - conformance profile identifier. Unsupported mandatory capabilities fail with an explicit stable problem code. They are never silently discarded. ## 11. Operational requirements A production exchange requires observable surfaces for: - authentication and authorization denials; - connector health and last successful exchange; - event delivery attempts and dead letters; - handovers awaiting acknowledgement; - quarantined mappings; - version conflicts; - reconciliation progress; - active commitments affected by organization, connector or relationship suspension. Logs MUST avoid exact locations, customer identities, contact details, sensitive cargo and commercial terms unless an explicitly authorized diagnostic mode is active. ## 12. Conformance scenarios The black-box conformance suite MUST cover at least: - capacity and demand discovery within an active relationship; - isolation from a non-partner organization; - tender acceptance, rejection, counterproposal, expiry and withdrawal; - commitment creation under each supported decision policy; - handover import and destination acknowledgement; - duplicate command, event and handover delivery; - amendment requiring renewed acceptance; - cancellation racing with acceptance or import; - connector outage followed by reconciliation; - relationship or credential suspension during active execution; - quarantined mapping without invented data; - bidirectional exchange between independently implemented clients. --- # Specification Source: specification/README.md Protocol specification artifacts for OpenFreytProtocol. ## Structure ``` specification/ ├── private-exchange-profile.md ← tender, commitment and execution-handover profile ├── protocol.md ← normative protocol clauses (RFC 2119 language) ├── glossary.md ← canonical term definitions ├── state-machines/ ← lifecycle state-transition specifications ├── schemas/ ← JSON Schema 2020-12 definitions ├── openapi/ ← OpenAPI 3.1 HTTP interface descriptions ├── events/ ← AsyncAPI / CloudEvents envelope definitions └── errors/ ← RFC 9457 error catalogue ``` All artifacts here are normative. The reference implementation is not normative. See QUALITY-STRATEGY.md §4 and §11. --- # Area.schema Source: specification/schemas/area.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/area", "title": "Area", "description": "A geographic area represented by at least one of: UN/LOCODE, ISO 3166-2 administrative region code, or a GeoJSON Polygon/MultiPolygon.", "type": "object", "properties": { "locode": { "type": "string", "description": "UN/LOCODE (e.g. NLRTM for Rotterdam).", "pattern": "^[A-Z]{2}[A-Z2-9]{3}$" }, "adminRegionCode": { "type": "string", "description": "ISO 3166-2 administrative region code (e.g. NL-ZH).", "pattern": "^[A-Z]{2}-[A-Z0-9]{1,3}$" }, "geoJson": { "type": "object", "description": "GeoJSON RFC 7946 Polygon or MultiPolygon.", "required": ["type", "coordinates"], "properties": { "type": { "type": "string", "enum": ["Polygon", "MultiPolygon"] }, "coordinates": { "type": "array" } } } } } --- # Capacity Offer.schema Source: specification/schemas/capacity-offer.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/capacity-offer", "title": "CapacityOffer", "description": "A carrier's offer of available transport capacity between two areas within a departure window.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency counter. Incremented on every mutation." }, "ownerOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that owns this resource." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." }, "externalReferences": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "Namespaced references to records in external systems (TMS, ERP, etc.)." }, "status": { "type": "string", "enum": ["draft", "published", "partiallyMatched", "matched", "withdrawn", "expired", "cancelled"], "description": "Lifecycle state of the capacity offer." }, "originArea": { "$ref": "https://openfreytprotocol.example/schemas/v0/area", "description": "Geographic area from which the vehicle departs." }, "destinationArea": { "$ref": "https://openfreytprotocol.example/schemas/v0/area", "description": "Geographic area where the vehicle arrives." }, "vehicleSpec": { "$ref": "https://openfreytprotocol.example/schemas/v0/vehicle-spec", "description": "Specification of the vehicle or vehicle type being offered." }, "departureWindowStart": { "type": "string", "format": "date-time", "description": "Earliest ISO 8601 date-time at which the vehicle is available to depart." }, "departureWindowEnd": { "type": "string", "format": "date-time", "description": "Latest ISO 8601 date-time at which the vehicle can depart." }, "availableCapacity": { "type": "object", "description": "The capacity currently available for loading.", "properties": { "weightKg": { "type": "number", "minimum": 0, "description": "Available weight capacity in kilograms." }, "volumeM3": { "type": "number", "minimum": 0, "description": "Available volume capacity in cubic metres." }, "loadingMetres": { "type": "number", "minimum": 0, "description": "Available loading metres on the trailer floor." } } } }, "required": [ "id", "resourceVersion", "ownerOrganizationId", "createdAt", "updatedAt", "status", "originArea", "destinationArea", "vehicleSpec", "departureWindowStart", "departureWindowEnd", "availableCapacity" ], "$defs": { "ExternalReference": { "type": "object", "properties": { "system": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "string" } }, "required": ["system", "type", "value"] } } } --- # Common Envelope.schema Source: specification/schemas/common-envelope.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/common-envelope", "title": "CommonEnvelope", "description": "Shared envelope included in every PCX resource. Resource schemas extend this by merging its properties rather than $ref-composing it, to avoid additionalProperties conflicts.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier. Opaque to consumers; ordering is time-monotonic." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Monotonically increasing optimistic-concurrency counter. Incremented by the broker on every mutation." }, "ownerOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that owns this resource." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." }, "externalReferences": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "Namespaced references to records in external systems (TMS, ERP, etc.)." } }, "required": [ "id", "resourceVersion", "ownerOrganizationId", "createdAt", "updatedAt" ], "$defs": { "ExternalReference": { "title": "ExternalReference", "description": "A typed pointer to a record in an external system.", "type": "object", "properties": { "system": { "type": "string", "description": "Identifier of the external system, e.g. 'carrier-a-tms' or 'shipper-b-erp'." }, "type": { "type": "string", "description": "Record type within the external system, e.g. 'trip', 'order', 'shipment'." }, "value": { "type": "string", "description": "Identifier value within the external system." } }, "required": ["system", "type", "value"] } } } --- # Capacity Offer Invalid Status.example Source: specification/schemas/examples/invalid/capacity-offer-invalid-status.example.json { "_comment": "INVALID: status 'invalid-status' is not in enum [draft, published, partiallyMatched, matched, withdrawn, expired, cancelled]", "id": "018e8e9c-dead-7000-beef-000000000002", "resourceVersion": 1, "ownerOrganizationId": "org-truck-co-001", "createdAt": "2026-06-01T08:00:00Z", "updatedAt": "2026-06-01T08:00:00Z", "status": "invalid-status", "originArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [8.682127, 50.110924] }, "properties": { "radiusKm": 50 } }, "destinationArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.404954, 52.520008] }, "properties": { "radiusKm": 50 } }, "vehicleSpec": { "vehicleBodyType": "tautliner" }, "departureWindowStart": "2026-06-10T06:00:00Z", "departureWindowEnd": "2026-06-10T18:00:00Z", "availableCapacity": { "weightKg": 24000 } } --- # Capacity Offer Missing Required.example Source: specification/schemas/examples/invalid/capacity-offer-missing-required.example.json { "_comment": "INVALID: missing required field 'departureWindowStart'", "id": "018e8e9c-dead-7000-beef-000000000002", "resourceVersion": 1, "ownerOrganizationId": "org-truck-co-001", "createdAt": "2026-06-01T08:00:00Z", "updatedAt": "2026-06-01T08:00:00Z", "status": "published", "originArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [8.682127, 50.110924] }, "properties": { "radiusKm": 50 } }, "destinationArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.404954, 52.520008] }, "properties": { "radiusKm": 50 } }, "vehicleSpec": { "vehicleBodyType": "tautliner" }, "departureWindowEnd": "2026-06-10T18:00:00Z", "availableCapacity": { "weightKg": 24000 } } --- # Execution Outcome Invalid Status.example Source: specification/schemas/examples/invalid/execution-outcome-invalid-status.example.json { "_comment": "INVALID: status 'pending' is not in enum [completed, cancelled, disputed]", "id": "018e8e9c-dead-7000-beef-000000000007", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-12T14:00:00Z", "updatedAt": "2026-06-12T14:00:00Z", "executionReferenceId": "018e8e9c-dead-7000-beef-000000000006", "status": "pending", "outcomes": {} } --- # Execution Outcome Missing Required.example Source: specification/schemas/examples/invalid/execution-outcome-missing-required.example.json { "_comment": "INVALID: missing required field 'status'", "id": "018e8e9c-dead-7000-beef-000000000007", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-12T14:00:00Z", "updatedAt": "2026-06-12T14:00:00Z", "executionReferenceId": "018e8e9c-dead-7000-beef-000000000006", "outcomes": {} } --- # Execution Reference Missing Required.example Source: specification/schemas/examples/invalid/execution-reference-missing-required.example.json { "_comment": "INVALID: missing required field 'matchId'", "id": "018e8e9c-dead-7000-beef-000000000006", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-03T08:00:00Z", "updatedAt": "2026-06-03T08:00:00Z", "externalTripRefs": [], "externalOrderRefs": [] } --- # Execution Reference Wrong Type.example Source: specification/schemas/examples/invalid/execution-reference-wrong-type.example.json { "_comment": "INVALID: matchId must be a string, not an integer", "id": "018e8e9c-dead-7000-beef-000000000006", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-03T08:00:00Z", "updatedAt": "2026-06-03T08:00:00Z", "matchId": 12345, "externalTripRefs": [], "externalOrderRefs": [] } --- # Load Request Invalid Status.example Source: specification/schemas/examples/invalid/load-request-invalid-status.example.json { "_comment": "INVALID: status 'shipped' is not in enum [draft, published, proposed, matched, withdrawn, expired, cancelled]", "id": "018e8e9c-dead-7000-beef-000000000003", "resourceVersion": 1, "ownerOrganizationId": "org-shipper-co-001", "createdAt": "2026-06-01T09:00:00Z", "updatedAt": "2026-06-01T09:00:00Z", "status": "shipped", "originArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [8.682127, 50.110924] }, "properties": { "radiusKm": 30 } }, "destinationArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.404954, 52.520008] }, "properties": { "radiusKm": 30 } }, "requiredCapacity": { "weightKg": 12000 }, "pickupWindowStart": "2026-06-10T07:00:00Z", "pickupWindowEnd": "2026-06-10T12:00:00Z", "deliveryWindowStart": "2026-06-11T06:00:00Z", "deliveryWindowEnd": "2026-06-11T18:00:00Z" } --- # Load Request Missing Required.example Source: specification/schemas/examples/invalid/load-request-missing-required.example.json { "_comment": "INVALID: missing required field 'originArea'", "id": "018e8e9c-dead-7000-beef-000000000003", "resourceVersion": 1, "ownerOrganizationId": "org-shipper-co-001", "createdAt": "2026-06-01T09:00:00Z", "updatedAt": "2026-06-01T09:00:00Z", "status": "published", "destinationArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.404954, 52.520008] }, "properties": { "radiusKm": 30 } }, "requiredCapacity": { "weightKg": 12000 }, "pickupWindowStart": "2026-06-10T07:00:00Z", "pickupWindowEnd": "2026-06-10T12:00:00Z", "deliveryWindowStart": "2026-06-11T06:00:00Z", "deliveryWindowEnd": "2026-06-11T18:00:00Z" } --- # Match Decision Invalid Decision.example Source: specification/schemas/examples/invalid/match-decision-invalid-decision.example.json { "_comment": "INVALID: decision 'abstain' is not in enum [accept, reject]", "id": "018e8e9c-dead-7000-beef-000000000005", "resourceVersion": 1, "ownerOrganizationId": "org-truck-co-001", "createdAt": "2026-06-02T11:00:00Z", "updatedAt": "2026-06-02T11:00:00Z", "proposalId": "018e8e9c-dead-7000-beef-000000000004", "proposalVersion": 1, "organizationId": "org-truck-co-001", "decision": "abstain", "decisionRole": "INITIATOR" } --- # Match Decision Stale Version.example Source: specification/schemas/examples/invalid/match-decision-stale-version.example.json { "_comment": "INVALID: proposalVersion 0 violates minimum: 1", "id": "018e8e9c-dead-7000-beef-000000000005", "resourceVersion": 1, "ownerOrganizationId": "org-truck-co-001", "createdAt": "2026-06-02T11:00:00Z", "updatedAt": "2026-06-02T11:00:00Z", "proposalId": "018e8e9c-dead-7000-beef-000000000004", "proposalVersion": 0, "organizationId": "org-truck-co-001", "decision": "accept", "decisionRole": "INITIATOR" } --- # Match Proposal Invalid Decision Policy.example Source: specification/schemas/examples/invalid/match-proposal-invalid-decision-policy.example.json { "_comment": "INVALID: decisionPolicy 'single' is not in enum [unanimous, majority, any]", "id": "018e8e9c-dead-7000-beef-000000000004", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-02T10:00:00Z", "updatedAt": "2026-06-02T10:00:00Z", "status": "open", "offerId": "018e8e9c-dead-7000-beef-000000000002", "requestId": "018e8e9c-dead-7000-beef-000000000003", "compatibilityChecks": [], "decisionPolicy": "single", "requiredDecisions": ["INITIATOR", "ACCEPTOR"] } --- # Match Proposal Missing Required.example Source: specification/schemas/examples/invalid/match-proposal-missing-required.example.json { "_comment": "INVALID: missing required field 'offerId'", "id": "018e8e9c-dead-7000-beef-000000000004", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-02T10:00:00Z", "updatedAt": "2026-06-02T10:00:00Z", "status": "open", "requestId": "018e8e9c-dead-7000-beef-000000000003", "compatibilityChecks": [], "decisionPolicy": "unanimous", "requiredDecisions": ["INITIATOR", "ACCEPTOR"] } --- # Partner Relationship Invalid Scope.example Source: specification/schemas/examples/invalid/partner-relationship-invalid-scope.example.json { "_comment": "INVALID: scope 'NO-COLON' does not match pattern ^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$", "id": "018e8e9c-dead-7000-beef-000000000001", "resourceVersion": 1, "requestingOrganizationId": "org-truck-co-001", "acceptingOrganizationId": "org-shipper-co-001", "status": "active", "scopes": ["NO-COLON"], "decisionPolicy": "unanimous", "effectiveFrom": "2026-01-01", "createdAt": "2026-01-01T00:00:00Z", "updatedAt": "2026-01-01T00:00:00Z" } --- # Partner Relationship Missing Required.example Source: specification/schemas/examples/invalid/partner-relationship-missing-required.example.json { "_comment": "INVALID: missing required field 'id'", "resourceVersion": 1, "requestingOrganizationId": "org-truck-co-001", "acceptingOrganizationId": "org-shipper-co-001", "status": "active", "scopes": ["capacity-offer:read"], "decisionPolicy": "unanimous", "effectiveFrom": "2026-01-01", "createdAt": "2026-01-01T00:00:00Z", "updatedAt": "2026-01-01T00:00:00Z" } --- # Capacity Offer.example Source: specification/schemas/examples/valid/capacity-offer.example.json { "id": "018e8e9c-dead-7000-beef-000000000002", "resourceVersion": 1, "ownerOrganizationId": "org-truck-co-001", "createdAt": "2026-06-01T08:00:00Z", "updatedAt": "2026-06-01T08:00:00Z", "status": "published", "originArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [8.682127, 50.110924] }, "properties": { "radiusKm": 50 } }, "destinationArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.404954, 52.520008] }, "properties": { "radiusKm": 50 } }, "vehicleSpec": { "vehicleBodyType": "tautliner", "capacity": { "weight": { "value": 24000, "unitCode": "KGM" }, "loadingMetres": { "value": 13.6, "unitCode": "MTR" } } }, "departureWindowStart": "2026-06-10T06:00:00Z", "departureWindowEnd": "2026-06-10T18:00:00Z", "availableCapacity": { "weightKg": 24000, "loadingMetres": 13.6 } } --- # Execution Outcome.example Source: specification/schemas/examples/valid/execution-outcome.example.json { "id": "018e8e9c-dead-7000-beef-000000000007", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-12T14:00:00Z", "updatedAt": "2026-06-12T14:00:00Z", "executionReferenceId": "018e8e9c-dead-7000-beef-000000000006", "status": "completed", "outcomes": { "actualPickupAt": "2026-06-10T09:15:00Z", "actualDeliveryAt": "2026-06-11T13:30:00Z" } } --- # Execution Reference.example Source: specification/schemas/examples/valid/execution-reference.example.json { "id": "018e8e9c-dead-7000-beef-000000000006", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-03T08:00:00Z", "updatedAt": "2026-06-03T08:00:00Z", "matchId": "018e8e9c-dead-7000-beef-000000000004", "externalTripRefs": [ { "system": "carrier-tms", "type": "trip", "value": "TRIP-2026-001" } ], "externalOrderRefs": [ { "system": "shipper-erp", "type": "order", "value": "ORD-2026-001" } ] } --- # Load Request.example Source: specification/schemas/examples/valid/load-request.example.json { "id": "018e8e9c-dead-7000-beef-000000000003", "resourceVersion": 1, "ownerOrganizationId": "org-shipper-co-001", "createdAt": "2026-06-01T09:00:00Z", "updatedAt": "2026-06-01T09:00:00Z", "status": "published", "originArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [8.682127, 50.110924] }, "properties": { "radiusKm": 30 } }, "destinationArea": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.404954, 52.520008] }, "properties": { "radiusKm": 30 } }, "requiredCapacity": { "weightKg": 12000, "volumeM3": 82 }, "pickupWindowStart": "2026-06-10T07:00:00Z", "pickupWindowEnd": "2026-06-10T12:00:00Z", "deliveryWindowStart": "2026-06-11T06:00:00Z", "deliveryWindowEnd": "2026-06-11T18:00:00Z" } --- # Match Decision.example Source: specification/schemas/examples/valid/match-decision.example.json { "id": "018e8e9c-dead-7000-beef-000000000005", "resourceVersion": 1, "ownerOrganizationId": "org-truck-co-001", "createdAt": "2026-06-02T11:00:00Z", "updatedAt": "2026-06-02T11:00:00Z", "proposalId": "018e8e9c-dead-7000-beef-000000000004", "proposalVersion": 1, "organizationId": "org-truck-co-001", "decision": "accept", "decisionRole": "INITIATOR" } --- # Match Proposal.example Source: specification/schemas/examples/valid/match-proposal.example.json { "id": "018e8e9c-dead-7000-beef-000000000004", "resourceVersion": 1, "ownerOrganizationId": "org-broker-001", "createdAt": "2026-06-02T10:00:00Z", "updatedAt": "2026-06-02T10:00:00Z", "status": "open", "offerId": "018e8e9c-dead-7000-beef-000000000002", "requestId": "018e8e9c-dead-7000-beef-000000000003", "compatibilityChecks": [ { "checkId": "vehicle-body-type-match", "result": "passed" }, { "checkId": "capacity-weight-sufficient", "result": "passed", "reason": "Available weight 24000 kg exceeds required 12000 kg." } ], "decisionPolicy": "unanimous", "requiredDecisions": ["INITIATOR", "ACCEPTOR"] } --- # Partner Relationship.example Source: specification/schemas/examples/valid/partner-relationship.example.json { "id": "018e8e9c-dead-7000-beef-000000000001", "resourceVersion": 1, "requestingOrganizationId": "org-truck-co-001", "acceptingOrganizationId": "org-shipper-co-001", "status": "active", "scopes": ["capacity-offer:read", "load-request:write"], "decisionPolicy": "unanimous", "effectiveFrom": "2026-01-01", "createdAt": "2026-01-01T00:00:00Z", "updatedAt": "2026-01-01T00:00:00Z" } --- # Execution Outcome.schema Source: specification/schemas/execution-outcome.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/execution-outcome", "title": "ExecutionOutcome", "description": "The final reported outcome of a transport execution, recorded after the trip has concluded, been cancelled, or entered dispute.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency counter. Incremented on every mutation." }, "ownerOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that owns this resource." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." }, "externalReferences": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "Namespaced references to records in external systems." }, "executionReferenceId": { "type": "string", "format": "uuid", "description": "Identifier of the ExecutionReference this outcome closes." }, "status": { "type": "string", "enum": ["completed", "cancelled", "disputed"], "description": "Final lifecycle status of the execution." }, "outcomes": { "type": "object", "description": "Structured outcome data reported by the parties. Content is broker-defined and may include actual weights, timestamps, POD references, or dispute evidence.", "properties": { "actualPickupAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of actual goods collection." }, "actualDeliveryAt": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of actual goods delivery." }, "actualWeightKg": { "type": "number", "minimum": 0, "description": "Actual transported weight in kilograms." }, "proofOfDeliveryRef": { "type": "string", "description": "Reference to a proof-of-delivery document in an external system." }, "cancellationReason": { "type": "string", "description": "Free-text reason for cancellation, populated when status is 'cancelled'." }, "disputeDescription": { "type": "string", "description": "Free-text description of the dispute, populated when status is 'disputed'." } } } }, "required": [ "id", "resourceVersion", "ownerOrganizationId", "createdAt", "updatedAt", "executionReferenceId", "status", "outcomes" ], "$defs": { "ExternalReference": { "type": "object", "properties": { "system": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "string" } }, "required": ["system", "type", "value"] } } } --- # Execution Reference.schema Source: specification/schemas/execution-reference.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/execution-reference", "title": "ExecutionReference", "description": "A broker record that links a confirmed match to the external trip and order identifiers created in the parties' operational systems.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency counter. Incremented on every mutation." }, "ownerOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that owns this resource." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." }, "externalReferences": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "Namespaced references to records in external systems." }, "matchId": { "type": "string", "format": "uuid", "description": "Identifier of the MatchProposal that was accepted and led to this execution." }, "externalTripRefs": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "References to trip records created in the carrier's TMS or similar operational system." }, "externalOrderRefs": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "References to order records created in the shipper's ERP or order management system." } }, "required": [ "id", "resourceVersion", "ownerOrganizationId", "createdAt", "updatedAt", "matchId", "externalTripRefs", "externalOrderRefs" ], "$defs": { "ExternalReference": { "type": "object", "properties": { "system": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "string" } }, "required": ["system", "type", "value"] } } } --- # Load Request.schema Source: specification/schemas/load-request.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/load-request", "title": "LoadRequest", "description": "A shipper's request for transport capacity between two areas within a pickup and delivery window.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency counter. Incremented on every mutation." }, "ownerOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that owns this resource." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." }, "externalReferences": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "Namespaced references to records in external systems (TMS, ERP, etc.)." }, "status": { "type": "string", "enum": ["draft", "published", "proposed", "matched", "withdrawn", "expired", "cancelled"], "description": "Lifecycle state of the load request." }, "originArea": { "$ref": "https://openfreytprotocol.example/schemas/v0/area", "description": "Geographic area from which the goods are collected." }, "destinationArea": { "$ref": "https://openfreytprotocol.example/schemas/v0/area", "description": "Geographic area to which the goods must be delivered." }, "requiredCapacity": { "type": "object", "description": "The capacity required to transport this load.", "properties": { "weightKg": { "type": "number", "minimum": 0, "description": "Required weight capacity in kilograms." }, "volumeM3": { "type": "number", "minimum": 0, "description": "Required volume capacity in cubic metres." }, "loadingMetres": { "type": "number", "minimum": 0, "description": "Required loading metres on the trailer floor." }, "vehicleBodyTypes": { "type": "array", "items": { "type": "string" }, "description": "Acceptable vehicle body types for this load." } } }, "pickupWindowStart": { "type": "string", "format": "date-time", "description": "Earliest ISO 8601 date-time at which goods can be collected." }, "pickupWindowEnd": { "type": "string", "format": "date-time", "description": "Latest ISO 8601 date-time by which goods must be collected." }, "deliveryWindowStart": { "type": "string", "format": "date-time", "description": "Earliest ISO 8601 date-time at which goods can be delivered." }, "deliveryWindowEnd": { "type": "string", "format": "date-time", "description": "Latest ISO 8601 date-time by which goods must be delivered." } }, "required": [ "id", "resourceVersion", "ownerOrganizationId", "createdAt", "updatedAt", "status", "originArea", "destinationArea", "requiredCapacity", "pickupWindowStart", "pickupWindowEnd", "deliveryWindowStart", "deliveryWindowEnd" ], "$defs": { "ExternalReference": { "type": "object", "properties": { "system": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "string" } }, "required": ["system", "type", "value"] } } } --- # Match Decision.schema Source: specification/schemas/match-decision.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/match-decision", "title": "MatchDecision", "description": "A party's accept or reject decision on a MatchProposal, including an optimistic concurrency check on the proposal version.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency counter. Incremented on every mutation." }, "ownerOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that owns this resource." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." }, "externalReferences": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "Namespaced references to records in external systems." }, "proposalId": { "type": "string", "format": "uuid", "description": "Identifier of the MatchProposal this decision refers to." }, "proposalVersion": { "type": "integer", "minimum": 1, "description": "resourceVersion of the MatchProposal at the time this decision was submitted. Used for optimistic concurrency: the broker rejects the decision if the proposal has since been superseded." }, "organizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization submitting this decision." }, "decision": { "type": "string", "enum": ["accept", "reject"], "description": "The party's decision on the proposal." }, "decisionRole": { "type": "string", "enum": ["INITIATOR", "ACCEPTOR"], "description": "The role of the deciding party within the match: INITIATOR posted the offer or request that triggered the match; ACCEPTOR is the counterparty." }, "reason": { "type": "string", "description": "Optional free-text reason for the decision, especially useful for rejections." } }, "required": [ "id", "resourceVersion", "ownerOrganizationId", "createdAt", "updatedAt", "proposalId", "proposalVersion", "organizationId", "decision", "decisionRole" ], "$defs": { "ExternalReference": { "type": "object", "properties": { "system": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "string" } }, "required": ["system", "type", "value"] } } } --- # Match Proposal.schema Source: specification/schemas/match-proposal.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/match-proposal", "title": "MatchProposal", "description": "A broker-generated proposal pairing a CapacityOffer with a LoadRequest, pending acceptance by both parties.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency counter. Incremented on every mutation." }, "ownerOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that owns this resource." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." }, "externalReferences": { "type": "array", "items": { "$ref": "#/$defs/ExternalReference" }, "description": "Namespaced references to records in external systems." }, "status": { "type": "string", "enum": ["open", "accepted", "rejected", "expired", "superseded", "withdrawn"], "description": "Lifecycle state of the match proposal." }, "offerId": { "type": "string", "format": "uuid", "description": "Identifier of the CapacityOffer included in this proposal." }, "requestId": { "type": "string", "format": "uuid", "description": "Identifier of the LoadRequest included in this proposal." }, "compatibilityChecks": { "type": "array", "description": "Results of automated compatibility checks run by the broker when generating this proposal.", "items": { "$ref": "#/$defs/CompatibilityCheck" } }, "decisionPolicy": { "type": "string", "enum": ["unanimous", "majority", "any"], "description": "Governs how MatchDecisions are aggregated to reach a final outcome for this proposal." }, "requiredDecisions": { "type": "array", "items": { "type": "string", "enum": ["INITIATOR", "ACCEPTOR"], "description": "Role identifier for a party whose MatchDecision is required." }, "uniqueItems": true, "description": "Set of party roles that must submit a MatchDecision before the proposal can be resolved." } }, "required": [ "id", "resourceVersion", "ownerOrganizationId", "createdAt", "updatedAt", "status", "offerId", "requestId", "compatibilityChecks", "decisionPolicy", "requiredDecisions" ], "$defs": { "ExternalReference": { "type": "object", "properties": { "system": { "type": "string" }, "type": { "type": "string" }, "value": { "type": "string" } }, "required": ["system", "type", "value"] }, "CompatibilityCheck": { "title": "CompatibilityCheck", "description": "Result of a single automated compatibility check.", "type": "object", "properties": { "checkId": { "type": "string", "description": "Stable identifier of the check rule, e.g. 'vehicle-body-type-match'." }, "result": { "type": "string", "enum": ["passed", "failed", "abstained"], "description": "Outcome of the check." }, "reason": { "type": "string", "description": "Optional human-readable explanation of why the check produced this result." } }, "required": ["checkId", "result"] } } } --- # Organization Reference.schema Source: specification/schemas/organization-reference.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/organization-reference", "title": "OrganizationReference", "description": "Reference to a participating organization, with an optional legal identifier for cross-system reconciliation.", "type": "object", "properties": { "organizationId": { "type": "string", "description": "Opaque broker-scoped organization identifier. Consumers must treat this as an untyped string." }, "legalIdentifier": { "$ref": "#/$defs/LegalIdentifier" } }, "required": ["organizationId"], "$defs": { "LegalIdentifier": { "title": "LegalIdentifier", "description": "A recognized legal or trade identifier for an organization.", "type": "object", "properties": { "type": { "type": "string", "enum": ["GLN", "EORI", "DUNS", "custom"], "description": "Identifier scheme. Use 'custom' when the scheme is proprietary or not yet in the standard enum." }, "value": { "type": "string", "description": "Value of the legal identifier within the chosen scheme." } }, "required": ["type", "value"] } } } --- # Partner Relationship.schema Source: specification/schemas/partner-relationship.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/partner-relationship", "title": "PartnerRelationship", "description": "A bilateral trust relationship between two organizations on a broker, governing which resource types and operations each party may expose to the other.", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "UUIDv7 resource identifier." }, "resourceVersion": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency counter. Incremented on every mutation." }, "requestingOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that initiated the relationship." }, "acceptingOrganizationId": { "type": "string", "description": "Opaque broker-scoped identifier of the organization that must accept or reject the relationship." }, "status": { "type": "string", "enum": ["draft", "requested", "active", "suspended", "terminated"], "description": "Lifecycle state of the partner relationship." }, "scopes": { "type": "array", "items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$", "description": "A scope token in namespace:action format, e.g. 'capacity-offer:read'." }, "uniqueItems": true, "description": "Set of permission scopes granted by this relationship." }, "decisionPolicy": { "type": "string", "enum": ["unanimous", "majority", "any"], "description": "Governs how multi-party decisions are resolved within this relationship." }, "effectiveFrom": { "type": "string", "format": "date", "description": "ISO 8601 date from which this relationship is effective." }, "effectiveUntil": { "type": "string", "format": "date", "description": "ISO 8601 date after which this relationship expires. Omit for open-ended relationships." }, "retentionPolicyRef": { "type": "string", "description": "Optional reference to a broker-defined data retention policy applied to resources shared under this relationship." }, "createdAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp when the resource was first created." }, "updatedAt": { "type": "string", "format": "date-time", "description": "ISO 8601 / RFC 3339 timestamp of the most recent mutation." } }, "required": [ "id", "resourceVersion", "requestingOrganizationId", "acceptingOrganizationId", "status", "scopes", "decisionPolicy", "effectiveFrom", "createdAt", "updatedAt" ] } --- # Problem Detail.schema Source: specification/schemas/problem-detail.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/problem-detail.schema.json", "title": "ProblemDetail", "description": "RFC 9457 Problem Details for HTTP APIs — base schema. Extended by the OpenFreyt Protocol error catalogue (specification/errors/error-catalogue.md) with protocol-specific type URIs and extension fields.", "type": "object", "required": ["type", "title", "status"], "properties": { "type": { "type": "string", "format": "uri", "description": "URI identifying the problem type. Protocol-specific codes use the URN scheme: urn:freyt:protocol:error:{code}." }, "title": { "type": "string", "description": "Short, human-readable summary of the problem type. Fixed per type URI; MUST NOT vary across occurrences of the same type." }, "status": { "type": "integer", "minimum": 400, "maximum": 599, "description": "HTTP status code generated by the origin server." }, "detail": { "type": "string", "description": "Human-readable explanation specific to this occurrence of the problem." }, "instance": { "type": "string", "format": "uri-reference", "description": "URI reference that identifies the specific occurrence of the problem." }, "correlationId": { "type": "string", "description": "Broker-assigned correlation identifier for distributed tracing and support lookup." }, "errors": { "type": "array", "description": "Field-level validation errors (present on type validation-error).", "items": { "type": "object", "required": ["field", "message"], "properties": { "field": { "type": "string", "description": "JSON Pointer (RFC 6901) to the field that failed validation." }, "message": { "type": "string", "description": "Human-readable description of the validation failure." }, "code": { "type": "string", "description": "Machine-readable error code for the validation failure." } } } } } } --- # Vehicle Spec.schema Source: specification/schemas/vehicle-spec.schema.json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openfreytprotocol.example/schemas/v0/vehicle-spec", "title": "VehicleSpec", "description": "Specification of a vehicle or trailer type offered or required in a capacity transaction. Body type is drawn from a normative enum; the 'extension:' prefix allows proprietary types without schema changes.", "type": "object", "properties": { "vehicleBodyType": { "description": "Normative vehicle/trailer body type. Use the 'extension:' pattern for types not in the standard enum.", "oneOf": [ { "type": "string", "enum": [ "tautliner", "curtainsider", "box", "refrigerated", "flatbed", "tanker", "bulk", "heavy-lift", "low-loader", "swap-body" ] }, { "type": "string", "pattern": "^extension:.+$", "description": "Proprietary extension type. Format: 'extension:/' is recommended." } ] }, "capacity": { "$ref": "#/$defs/Capacity" } }, "required": ["vehicleBodyType"], "$defs": { "Capacity": { "title": "Capacity", "description": "Dimensional and weight capacity of the vehicle. All sub-dimensions are optional individually, but at least one should be present in practice.", "type": "object", "properties": { "weight": { "$ref": "#/$defs/Measure", "description": "Maximum payload weight. Recommended unit code: 'KGM' (kilogram) per UN/CEFACT." }, "volume": { "$ref": "#/$defs/Measure", "description": "Internal loading volume. Recommended unit code: 'MTQ' (cubic metre) per UN/CEFACT." }, "loadingMetres": { "$ref": "#/$defs/Measure", "description": "Loading metre capacity (effective floor length usable for loading). Recommended unit code: 'MTR' (metre) per UN/CEFACT." } } }, "Measure": { "title": "Measure", "description": "A numeric quantity with a UN/CEFACT unit code.", "type": "object", "properties": { "value": { "type": "number", "minimum": 0, "description": "Non-negative numeric quantity." }, "unitCode": { "type": "string", "description": "UN/CEFACT Recommendation 20 unit code, e.g. 'KGM', 'MTQ', 'MTR'." } }, "required": ["value", "unitCode"] } } } --- # CapacityOffer — Lifecycle State Machine Source: specification/state-machines/capacity-offer.md **RFC 2119 normative language applies throughout.** ## 1. States | State | Meaning | |-------|---------| | `draft` | Offer assembled but not yet published. Visible only to the owning (carrier) organization. | | `published` | Offer is live and visible to authorized partners. Available for matching. | | `partiallyMatched` | One or more load requests have been tentatively matched but capacity is not fully committed. Further matching is still permitted. | | `matched` | All available capacity is committed to one or more accepted matches. New matching is blocked. | | `withdrawn` | Carrier voluntarily removed the offer before or after matching. No further commands are accepted. | | `expired` | `expiresAt` passed before the offer was matched or withdrawn. No further commands are accepted. | | `cancelled` | Administratively cancelled (e.g. due to policy violation or erroneous entry). No further commands are accepted. | **Terminal states:** `withdrawn`, `expired`, `cancelled` ## 2. State-Transition Table | From State | Command | To State | Notes | |------------|---------|----------|-------| | *(none)* | `POST /capacity-offers` | `draft` | Owning organization creates the offer. | | `draft` | `publish` (`POST /capacity-offers/{id}/publish`) | `published` | Carrier makes offer visible to authorized partners. | | `published` | `partialMatch` *(system)* | `partiallyMatched` | System records a tentative match; capacity partially consumed. | | `published` | `fullMatch` *(system)* | `matched` | System commits all available capacity to accepted matches. | | `published` | `withdraw` (`POST /capacity-offers/{id}/withdraw`) | `withdrawn` | Carrier withdraws the live offer. | | `published` | `expire` *(system, clock)* | `expired` | `expiresAt` reached with no accepted match. | | `partiallyMatched` | `fullMatch` *(system)* | `matched` | Remaining capacity committed. | | `partiallyMatched` | `withdraw` | `withdrawn` | Carrier withdraws; any tentative matches are cancelled. | | `partiallyMatched` | `expire` *(system, clock)* | `expired` | `expiresAt` reached. | | `matched` | `unmatch` *(system, on match cancellation)* | `partiallyMatched` | An accepted match is later cancelled releasing partial capacity. | | `draft` | `cancel` *(admin)* | `cancelled` | Administrative cancellation before publication. | | `published` | `cancel` *(admin)* | `cancelled` | Administrative cancellation after publication. | | `partiallyMatched` | `cancel` *(admin)* | `cancelled` | Administrative cancellation; tentative matches voided. | ### Invalid Transitions Any command not listed above MUST return `422 Unprocessable Entity` with error type `urn:freyt:protocol:error:invalid-state-transition`. Attempting to publish an already-published offer returns `409 Conflict` with `idempotency-conflict` if the same `Idempotency-Key` is reused, or `422` otherwise. ## 3. Preconditions ### `publish` (draft → published) - **Authorization:** Actor belongs to the owning organization. - **Scope required:** `capacity:publish` - **Relationship state:** Owning organization must have at least one `active` partner relationship unless the offer uses an open visibility policy. - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** `availabilityWindow`, `originArea`, `destinationArea`, `vehicleProfile`, `availableCapacity`, `expiresAt`, and `visibility` MUST all be present and valid. ### `withdraw` (published | partiallyMatched → withdrawn) - **Authorization:** Actor belongs to the owning organization. - **Scope required:** `capacity:publish` - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** Withdrawal reason MUST be provided. Accepted matches dependent on this offer MUST be transitioned to cancelled before or atomically with this withdrawal. ### `partialMatch` / `fullMatch` (system commands) - **Authorization:** Broker system only. - **Precondition:** At least one `LoadRequest` in `published` state is compatible and the relationship between orgs is `active`. - **Version check:** Both offer and request `resourceVersion` values MUST be observed before matching and included in the `MatchProposal`. ### `expire` (system command) - **Authorization:** System clock process. - **Precondition:** Current time ≥ `expiresAt`. - **Version check:** No `If-Match` required for system clock transitions. ### `cancel` (admin) - **Authorization:** Protocol operator with admin scope. - **Scope required:** `capacity:publish` + operator flag. - **Policy check:** Cancellation reason MUST be recorded. ## 4. Postconditions | Command | Postconditions | |---------|---------------| | `POST /capacity-offers` | `status` = `draft`; `resourceVersion` = 1; `createdAt` recorded. | | `publish` | `status` = `published`; `publishedAt` recorded; `resourceVersion` incremented; event `pcx.capacity-offer.published.v1` emitted. | | `partialMatch` | `status` = `partiallyMatched`; matched capacity dimensions updated; `resourceVersion` incremented; event `pcx.capacity-offer.partially-matched.v1` emitted. | | `fullMatch` | `status` = `matched`; `matchedAt` recorded; `resourceVersion` incremented; event `pcx.capacity-offer.matched.v1` emitted. | | `withdraw` | `status` = `withdrawn`; `withdrawnAt` recorded; withdrawal reason stored; `resourceVersion` incremented; event `pcx.capacity-offer.withdrawn.v1` emitted. | | `expire` | `status` = `expired`; `expiredAt` recorded; `resourceVersion` incremented; event `pcx.capacity-offer.expired.v1` emitted. | | `unmatch` | `status` reverts to `partiallyMatched` (or `published` if no remaining matches); `resourceVersion` incremented. | | `cancel` | `status` = `cancelled`; cancellation reason and actor recorded; `resourceVersion` incremented. | ## 5. Concurrency Rules - All state-changing commands require `If-Match` with the current ETag value. - **Concurrent publish and withdraw:** first writer wins; second receives `409 Conflict` with `stale-resource-version`. - **Concurrent matching:** the broker MUST use optimistic concurrency on both offer and request. A race between two match attempts on the same offer is resolved by the first committed transaction; the second attempt MUST re-read the updated offer version and re-evaluate compatibility before retrying. - **Withdraw racing with match commit:** if a `withdraw` command and a `fullMatch` system event race, the outcome is determined by which transaction commits first. A committed match MUST NOT be reversed by a delayed withdrawal; the carrier MUST be notified and a separate cancellation flow initiated. - **Expire racing with withdraw:** system `expire` and a carrier `withdraw` may race. Either outcome is valid; the resulting terminal state is recorded verbatim. - Transitions MUST be applied atomically within a single serializable transaction. ## 6. Invariants 1. **Capacity monotonicity:** `availableCapacity` in state `matched` MUST be ≤ `availableCapacity` at publication. 2. **No re-publication:** A `withdrawn`, `expired`, or `cancelled` offer MUST NOT be re-published. A new offer MUST be created. 3. **ExpiresAt immutability:** `expiresAt` MUST NOT be changed after the offer reaches `published`. 4. **Visibility isolation:** A `draft` offer MUST NOT be visible to any partner organization. 5. **Active relationship required:** Matching MUST only proceed if the carrier and shipper have an `active` `PartnerRelationship` covering the `capacity:publish` and `loads:read` scopes. 6. **Partial-match consistency:** The sum of capacity allocated to open `MatchProposal` records MUST NOT exceed `availableCapacity` at time of each match attempt. --- # ExecutionOutcome — Append-Only Corrections Model Source: specification/state-machines/execution-outcome.md **RFC 2119 normative language applies throughout.** > `ExecutionOutcome` follows an **append-only corrections model**. A submitted outcome is never mutated in place. Corrections are expressed as new records that supersede prior ones. This document describes the submission chain, correction types, preconditions, postconditions, and invariants. ## 1. Nature of the Record An `ExecutionOutcome` records whether and how an accepted exchange was executed, without copying participants' complete operational records. The record is logically associated with one `ExecutionReference`. The outcome history forms an ordered chain: ``` Outcome-1 (initial submission) └─ Outcome-2 (correction, supersedes Outcome-1) └─ Outcome-3 (further correction, supersedes Outcome-2) ``` At any moment, the **head** of the chain (the record with no `supersededByOutcomeId`) is the current authoritative outcome. ## 2. Outcome Status Values Although there is no single lifecycle automaton, each `ExecutionOutcome` record carries a `status` reflecting the execution reality it reports: | `status` Value | Meaning | |---------------|---------| | `planned` | Execution accepted but not yet started. | | `inProgress` | Execution underway; partial confirmation available. | | `completed` | Execution concluded successfully. | | `cancelled` | Execution was called off after `ExecutionReference` was created. | | `disputed` | One or more parties dispute the reported outcome; arbitration pending. | These values describe the **execution event** being reported, not the record's own lifecycle. A `completed` outcome record can still be superseded by a correction if the reported data was erroneous. ## 3. Submission Chain Operations ### 3.1 Initial Submission (`POST /execution-outcomes`) Creates the first `ExecutionOutcome` for a given `ExecutionReference`. **Preconditions:** - **Authorization:** Actor belongs to one of the `participatingOrganizationIds` of the linked `ExecutionReference`. - **Scope required:** `outcomes:write` - **Reference state:** The linked `ExecutionReference` MUST be in `linked` or `created` state (not yet `completed` or `cancelled`). - **Version check:** `executionReferenceId` and its current `resourceVersion` MUST be included. - **Deduplication key:** `deduplicationKey` (a stable caller-supplied key) MUST be provided. A duplicate submission with the same `deduplicationKey` returns the original record (`200 OK`). - **Policy check:** `status`, planned and actual time ranges (where available), and participant confirmations MUST be present. Sensitive fields (exact addresses, customer identity, price) MUST NOT be included. **Postconditions:** - New `ExecutionOutcome` record created; `submittedAt` recorded; `resourceVersion` = 1; `supersedes` = null. - `ExecutionReference` status transitions to `linked` if it was `created`, or remains `linked`. - If `status` is `completed` or `cancelled`, `ExecutionReference` MAY transition to `completed` or `cancelled` respectively (broker policy determines exact trigger). - Event `pcx.execution-outcome.recorded.v1` emitted. ### 3.2 Correction (`POST /execution-outcomes/{id}/corrections`) Creates a new `ExecutionOutcome` that supersedes the identified prior record. **Preconditions:** - **Authorization:** Actor belongs to one of the `participatingOrganizationIds` of the linked `ExecutionReference`. - **Scope required:** `outcomes:write` - **Prior record state:** The `{id}` in the path MUST identify the current head of the correction chain (i.e. the record with no `supersededByOutcomeId`). Correcting a non-head record MUST return `409 Conflict` with `idempotency-conflict`. - **Version check:** `observedOutcomeVersion` MUST match the `resourceVersion` of the record being superseded. A mismatch MUST return `409 Conflict` with `stale-resource-version`. - **Correction reason:** `correctionReason` MUST be provided. `correctionReasonCode` SHOULD be provided from the correction vocabulary. - **Deduplication key:** A new `deduplicationKey` MUST be supplied for this correction record. **Postconditions:** - New `ExecutionOutcome` record created; `submittedAt` recorded; `supersedes` = `{id}`; `resourceVersion` = 1 on the new record. - Prior record's `supersededByOutcomeId` set to the new record's `id` (immutable pointer added); prior record is otherwise unchanged. - If `status` of the new record is `completed` or `cancelled`, appropriate `ExecutionReference` transitions may be triggered. - Event `pcx.execution-outcome.corrected.v1` emitted, referencing both the old and new record IDs. ## 4. Correction Chain Fields ### Per-Record Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `id` | string (UUID) | yes | Stable immutable identifier for this outcome record. | | `executionReferenceId` | string | yes | Reference to the parent `ExecutionReference`. | | `status` | enum (see §2) | yes | Execution status being reported. | | `submittedAt` | RFC 3339 timestamp | yes | When this record was recorded (server-assigned). | | `submittedBy` | actor reference | yes | The actor submitting this record. | | `deduplicationKey` | string | yes | Caller-supplied stable key for idempotent submission. | | `plannedTimeRange` | time range | no | Planned start and end times. | | `actualTimeRange` | time range | no | Actual start and end times; required if `status` is `completed`. | | `participantConfirmations` | array | no | Per-organization confirmation records. | | `externalExecutionRefs` | array | no | External system references (namespaced). | | `operationalDeviations` | array | no | Documented deviations or exceptions. | | `capacityUtilizationObservations` | object | no | Optional observed utilization data. | | `routeObservations` | object | no | Optional route deviation or mileage data. | | `schemaVersion` | string | yes | Protocol schema version used for this record. | | `supersedes` | string (UUID) | no | ID of the prior record this one corrects; null for initial submissions. | | `supersededByOutcomeId` | string (UUID) | no | ID of the next record in the chain; null if this is the head. Set by the server; never provided by the client. | | `correctionReason` | string | conditional | Required when `supersedes` is set. | | `correctionReasonCode` | string | conditional | Structured reason code; SHOULD be provided when `supersedes` is set. | | `observedOutcomeVersion` | integer | conditional | Required when `supersedes` is set; MUST match `resourceVersion` of superseded record. | ## 5. Concurrency Rules - **Concurrent corrections on the same head:** Two concurrent `POST /execution-outcomes/{id}/corrections` against the same head record MUST be serialized. The first writer creates a new head and sets `supersededByOutcomeId` on the old head. The second writer's `observedOutcomeVersion` will no longer match; it MUST return `409 Conflict` with `stale-resource-version`. The caller MUST re-read the current head and retry if the correction is still needed. - **Concurrent initial submissions with the same `deduplicationKey`:** Only one record is created; subsequent requests with the same key return the original (`200 OK`). - **Concurrent initial submissions with different `deduplicationKey` values:** Both are accepted; however, a `(executionReferenceId, status=completed)` record SHOULD be treated as idempotent by the broker — only the first `completed` outcome triggers `ExecutionReference` completion. - **Dispute and correction racing:** A `disputed` outcome may be corrected; the correction supersedes the disputed record and MUST include a `correctionReason` explaining the resolution. ## 6. Invariants 1. **Append-only chain:** No `ExecutionOutcome` record MUST be mutated or deleted after recording. The `supersededByOutcomeId` pointer MAY be set by the server when a correction arrives, but all other fields are immutable. 2. **Single head invariant:** At most one record per `executionReferenceId` MUST have `supersededByOutcomeId` = null at any time. Two concurrent corrections to the same head violate this invariant and MUST be prevented by the `stale-resource-version` check. 3. **Correction chain integrity:** The chain formed by `supersedes` pointers MUST be acyclic and MUST eventually terminate at a record with `supersedes` = null (the initial submission). 4. **Deduplication key uniqueness:** `deduplicationKey` MUST be unique per `executionReferenceId`. The server MUST reject a new record whose `deduplicationKey` matches an existing record unless the request is an exact replay (same payload, returns `200 OK`). 5. **Schema version recording:** `schemaVersion` MUST be stored on every record so historical records remain interpretable after protocol version advances. 6. **Data minimisation:** `ExecutionOutcome` records MUST NOT contain customer identity, exact addresses, price, or complete goods details unless policy explicitly permits disclosure. Adapter-specific fields MUST be in the `extensions` namespace. 7. **Audit retention:** All records in the correction chain MUST be retained for the period defined in the `retentionPolicyRef` of the linked `PartnerRelationship`. 8. **Outcome-reference consistency:** A `completed` `ExecutionReference` MUST have a head `ExecutionOutcome` with `status` of `completed` or `cancelled`. The broker MUST NOT mark the reference `completed` based on a superseded outcome record. --- # ExecutionReference — Lifecycle State Machine Source: specification/state-machines/execution-reference.md **RFC 2119 normative language applies throughout.** ## 1. States | State | Meaning | |-------|---------| | `created` | `ExecutionReference` record has been created following a proposal acceptance. External execution records have not yet been linked. | | `linked` | At least one external execution record (TMS trip, order, or equivalent) has been linked. Execution is in progress. | | `completed` | Execution has concluded and all external references are confirmed. An `ExecutionOutcome` has been submitted. | | `cancelled` | Execution was called off after the match was committed. No further changes are expected. | **Terminal states:** `completed`, `cancelled` ## 2. State-Transition Table | From State | Command | To State | Notes | |------------|---------|----------|-------| | *(none)* | `POST /execution-references` | `created` | Created automatically when a `MatchProposal` is accepted, or posted explicitly by an authorized party. | | `created` | `link` *(via external reference attachment)* | `linked` | One or more external execution records attached. | | `created` | `cancel` | `cancelled` | Execution cancelled before any linking. Cancellation reason MUST be provided. | | `linked` | `complete` *(system or outcome submitter)* | `completed` | Final `ExecutionOutcome` submitted and confirmed. | | `linked` | `cancel` | `cancelled` | Execution cancelled after linking; reason MUST be provided. | ### Invalid Transitions Any command not listed above MUST return `422 Unprocessable Entity` with error type `urn:freyt:protocol:error:invalid-state-transition`. A `completed` or `cancelled` record MUST NOT accept further state-changing commands. ## 3. Preconditions ### `POST /execution-references` (→ created) - **Authorization:** Actor belongs to one of the participating organizations (carrier or shipper) or is the broker system performing automatic creation on proposal acceptance. - **Scope required:** `outcomes:write` - **Relationship state:** The linked `MatchProposal` MUST be in `accepted` state. - **Version check:** The `acceptedProposalId` and its `resourceVersion` MUST be recorded. - **Policy check:** `participatingOrganizationIds` MUST include both the carrier and shipper from the accepted proposal. ### `link` (created → linked) - **Authorization:** Actor belongs to one of the `participatingOrganizationIds`. - **Scope required:** `outcomes:write` - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** At least one valid external reference (`externalTripRef` or `externalOrderRef`) MUST be provided. References are namespaced with `system` and `type` fields to avoid collision across TMS systems. ### `complete` (linked → completed) - **Authorization:** Actor belongs to one of the `participatingOrganizationIds`, or triggered by the system after a final `ExecutionOutcome` is submitted and confirmed. - **Scope required:** `outcomes:write` - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** At least one `ExecutionOutcome` in a final state MUST reference this `ExecutionReference`. ### `cancel` (created | linked → cancelled) - **Authorization:** Actor belongs to one of the `participatingOrganizationIds`. - **Scope required:** `outcomes:write` - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** Cancellation reason MUST be provided. If `status` is `linked`, all participating organizations SHOULD be notified. ## 4. Postconditions | Command | Postconditions | |---------|---------------| | `POST /execution-references` | `status` = `created`; `resourceVersion` = 1; `createdAt` recorded; `acceptedProposalId` stored; participating organizations recorded. Event `pcx.execution-reference.created.v1` emitted. | | `link` | `status` = `linked`; `linkedAt` recorded; external references appended; `resourceVersion` incremented. Event `pcx.execution-reference.linked.v1` emitted. | | `complete` | `status` = `completed`; `completedAt` recorded; `resourceVersion` incremented. Event `pcx.execution.completed.v1` emitted. | | `cancel` | `status` = `cancelled`; `cancelledAt` recorded; cancellation reason stored; `resourceVersion` incremented. Event `pcx.execution-reference.cancelled.v1` emitted. | ## 5. Concurrency Rules - All state-changing commands require `If-Match` with the current ETag. - **Concurrent link and cancel:** first writer wins; second receives `409 Conflict` with `stale-resource-version`. - **Concurrent complete and cancel:** complete and cancel may race if an outcome is submitted at the same time a cancellation is requested. The first committed transaction wins. A committed `completed` state MUST NOT be overwritten by a delayed `cancel`. - **Multiple link calls:** Multiple external references may be attached in separate `link` calls. Each call MUST include `If-Match` against the version after the previous link. Concurrent link calls from different organizations must be serialized through optimistic concurrency; the losing call receives `409` and MUST retry with the updated version. - All transitions MUST be applied atomically. ## 6. Invariants 1. **Proposal binding:** An `ExecutionReference` MUST be associated with exactly one `accepted` `MatchProposal`. It MUST NOT be created from a proposal in any other state. 2. **External reference namespacing:** All external references MUST carry `system` and `type` identifiers. The server MUST NOT treat references from different systems as equivalent. 3. **No resurrection:** A `completed` or `cancelled` record MUST NOT be transitioned to any other state. 4. **Outcome consistency:** A `completed` `ExecutionReference` MUST have at least one `ExecutionOutcome` in a final state that references it. 5. **Organizational isolation:** External references MUST only be visible to the `participatingOrganizationIds` and authorized protocol operators. Broker logs MUST NOT expose exact location or customer identity contained in external system references. 6. **Audit retention:** `ExecutionReference` records MUST be retained as per the `retentionPolicyRef` derived from the linked relationship, even after `completed` or `cancelled`. --- # LoadRequest — Lifecycle State Machine Source: specification/state-machines/load-request.md **RFC 2119 normative language applies throughout.** ## 1. States | State | Meaning | |-------|---------| | `draft` | Request assembled but not yet published. Visible only to the owning (shipper) organization. | | `published` | Request is live and visible to authorized partners. Available for matching. | | `proposed` | At least one `MatchProposal` has been created linking this request to a `CapacityOffer`. Awaiting decision. | | `matched` | An accepted `MatchProposal` has committed this request to a carrier. No further matching is performed. | | `withdrawn` | Shipper voluntarily removed the request before fulfillment. No further commands are accepted. | | `expired` | `expiresAt` passed before the request was matched or withdrawn. No further commands are accepted. | | `cancelled` | Administratively cancelled (e.g. due to policy violation or erroneous entry). No further commands are accepted. | **Terminal states:** `withdrawn`, `expired`, `cancelled` ## 2. State-Transition Table | From State | Command | To State | Notes | |------------|---------|----------|-------| | *(none)* | `POST /load-requests` | `draft` | Owning organization creates the request. | | `draft` | `publish` (`POST /load-requests/{id}/publish`) | `published` | Shipper makes request visible to authorized partners. | | `published` | `propose` *(system)* | `proposed` | Broker creates a `MatchProposal`; request enters negotiation. | | `published` | `withdraw` (`POST /load-requests/{id}/withdraw`) | `withdrawn` | Shipper withdraws the live request. | | `published` | `expire` *(system, clock)* | `expired` | `expiresAt` reached with no accepted match. | | `proposed` | `accept` *(system, on MatchProposal accepted)* | `matched` | Associated `MatchProposal` is accepted by all required parties. | | `proposed` | `proposalExpired` *(system)* | `published` | All open proposals for this request expired or were rejected; request re-enters matching pool. | | `proposed` | `withdraw` | `withdrawn` | Shipper withdraws even while proposals are outstanding; open proposals are voided. | | `proposed` | `expire` *(system, clock)* | `expired` | `expiresAt` reached while proposals pending. | | `draft` | `cancel` *(admin)* | `cancelled` | Administrative cancellation before publication. | | `published` | `cancel` *(admin)* | `cancelled` | Administrative cancellation after publication. | | `proposed` | `cancel` *(admin)* | `cancelled` | Administrative cancellation; open proposals are voided. | ### Invalid Transitions Any command not listed above MUST return `422 Unprocessable Entity` with error type `urn:freyt:protocol:error:invalid-state-transition`. ## 3. Preconditions ### `publish` (draft → published) - **Authorization:** Actor belongs to the owning (shipper) organization. - **Scope required:** `loads:publish` - **Relationship state:** Owning organization must have at least one `active` partner relationship unless the request uses an open visibility policy. - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** `pickupArea`, `pickupWindow`, `deliveryArea`, `deliveryWindow`, `requiredCapacity`, `vehicleRequirements`, `goodsProfile`, `expiresAt`, and `visibility` MUST all be present and valid. Customer identity, exact addresses, price, and complete goods details MUST NOT appear unless policy explicitly permits disclosure. ### `withdraw` (published | proposed → withdrawn) - **Authorization:** Actor belongs to the owning organization. - **Scope required:** `loads:publish` - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** Withdrawal reason MUST be provided. Any open `MatchProposal` records linked to this request MUST be transitioned to `withdrawn` before or atomically with this command. ### `propose` (system command) - **Authorization:** Broker system only. - **Precondition:** At least one `CapacityOffer` in `published` or `partiallyMatched` state is compatible; the `PartnerRelationship` between carrier and shipper is `active`. - **Version check:** Both offer and request `resourceVersion` values MUST be observed and included in the `MatchProposal`. ### `accept` (system command, triggered by MatchProposal acceptance) - **Authorization:** Broker system only, triggered after all required `MatchDecision` records agree. - **Precondition:** Associated `MatchProposal` status is transitioning to `accepted`. - **Version check:** Request `resourceVersion` at time of proposal creation MUST match current version; otherwise `409 stale-resource-version`. ### `expire` (system command) - **Authorization:** System clock process. - **Precondition:** Current time ≥ `expiresAt`. ### `cancel` (admin) - **Authorization:** Protocol operator with admin scope. - **Policy check:** Cancellation reason MUST be recorded. ## 4. Postconditions | Command | Postconditions | |---------|---------------| | `POST /load-requests` | `status` = `draft`; `resourceVersion` = 1; `createdAt` recorded. | | `publish` | `status` = `published`; `publishedAt` recorded; `resourceVersion` incremented; event `pcx.load-request.published.v1` emitted. | | `propose` | `status` = `proposed`; `proposalIds` list updated; `resourceVersion` incremented; event `pcx.match-proposal.created.v1` emitted (on the proposal, not the request). | | `accept` | `status` = `matched`; `matchedAt` recorded; accepted `proposalId` stored; `resourceVersion` incremented; event `pcx.load-request.matched.v1` emitted. | | `proposalExpired` | `status` reverts to `published`; expired proposal removed from active proposal list; `resourceVersion` incremented. | | `withdraw` | `status` = `withdrawn`; `withdrawnAt` recorded; withdrawal reason stored; `resourceVersion` incremented; event `pcx.load-request.withdrawn.v1` emitted. | | `expire` | `status` = `expired`; `expiredAt` recorded; `resourceVersion` incremented; event `pcx.load-request.expired.v1` emitted. | | `cancel` | `status` = `cancelled`; cancellation reason and actor recorded; `resourceVersion` incremented. | ## 5. Concurrency Rules - All state-changing commands require `If-Match` with the current ETag. - **Concurrent publish and withdraw:** first writer wins; second receives `409 Conflict` with `stale-resource-version`. - **Concurrent match attempts:** the broker MUST use optimistic concurrency on both request and offer. If a second match attempt reads a version that was already updated by the first committed match, it MUST abort and not create a duplicate `MatchProposal`. - **Withdraw racing with match acceptance:** a `withdraw` and a concurrent `accept` (from a completed proposal decision) race. The first committed transaction wins. A committed `matched` state MUST NOT be reversed by a delayed `withdraw`; the shipper MUST be notified and a separate cancellation initiated. - **Expire racing with accept:** if the request expires while a proposal acceptance is being committed, the expiry MUST NOT overwrite the `matched` state. The `matched` terminal state takes precedence if committed first. - All transitions MUST be applied atomically. ## 6. Invariants 1. **Single active match:** A `LoadRequest` in `matched` state MUST be associated with exactly one accepted `MatchProposal`. 2. **No re-publication:** A `withdrawn`, `expired`, or `cancelled` request MUST NOT be re-published. A new request MUST be created. 3. **ExpiresAt immutability:** `expiresAt` MUST NOT be changed after the request reaches `published`. 4. **Visibility isolation:** A `draft` request MUST NOT be visible to any partner organization. 5. **Data minimisation:** `goodsProfile` in a published request MUST contain only coarse, non-sensitive classification. Exact goods details, customer identity, and price MUST NOT be included unless policy permits. 6. **Active relationship required:** A `MatchProposal` linking this request to a `CapacityOffer` MUST only be created when the owning organizations have an `active` `PartnerRelationship`. 7. **Proposal consistency:** The set of `proposalIds` on a `proposed` request MUST only reference `MatchProposal` records that themselves reference this `loadRequestId`. --- # MatchDecision — Decision Type Specification Source: specification/state-machines/match-decision.md **RFC 2119 normative language applies throughout.** > `MatchDecision` is an **append-only** record. It has no lifecycle state machine. Once recorded, a `MatchDecision` MUST NOT be mutated or deleted. This document describes the decision types, their preconditions, postconditions, and how the decision record drives the `MatchProposal` state machine. ## 1. Nature of the Record A `MatchDecision` is an immutable decision event appended to a proposal. Its purpose is to record the explicit, auditable intent of an authorized actor at a specific point in time. The broker evaluates the set of decisions against the `requiredDecisionPolicy` of the linked `MatchProposal` and triggers the appropriate proposal state transition. **There is no `status` field on `MatchDecision`.** The decision itself is the terminal artifact. ## 2. Decision Types | `decision` Value | Meaning | |-----------------|---------| | `accept` | The deciding organization accepts the proposed match. If policy is satisfied, the proposal transitions to `accepted`. | | `reject` | The deciding organization declines the proposed match. The proposal transitions to `rejected`. | | `withdraw` | The deciding organization retracts a previously submitted `accept` decision, provided the proposal has not yet been committed. See withdrawal rules below. | ### `accept` An `accept` decision signals that the deciding organization agrees to the terms of the `MatchProposal` as of the observed proposal version. **Preconditions:** - **Authorization:** Actor belongs to `organizationId` named in the decision. The organization MUST be a required party under `requiredDecisionPolicy`. - **Scope required:** `matches:decide` - **Proposal state:** `MatchProposal` MUST be in `open` state. A decision against a non-`open` proposal MUST return `422` with `invalid-state-transition`. - **Version check:** `observedProposalVersion` MUST match the current `resourceVersion` of the proposal. A mismatch MUST return `409 Conflict` with `stale-resource-version`. The server MUST NOT silently apply a decision made against a changed proposal. - **Idempotency:** A duplicate `accept` from the same organization with the same `Idempotency-Key` returns the original record. A second `accept` from the same organization without an idempotency key MUST return `409 idempotency-conflict` if one already exists. **Postconditions:** - Decision record appended with `decidedAt` timestamp, `decidedBy` actor reference, and `observedProposalVersion`. - Broker re-evaluates whether all required `accept` decisions are now recorded. If policy is satisfied, the proposal transitions to `accepted` atomically. - Event `pcx.match-decision.recorded.v1` emitted. ### `reject` A `reject` decision signals that the deciding organization declines the proposed match and the negotiation for this proposal ends. **Preconditions:** - **Authorization:** Actor belongs to `organizationId`. The organization MUST be a required party under `requiredDecisionPolicy`. - **Scope required:** `matches:decide` - **Proposal state:** `MatchProposal` MUST be in `open` state. - **Version check:** `observedProposalVersion` MUST match current `resourceVersion`. - **Policy check:** `reasonCode` is optional but SHOULD be provided. `comment` is access-controlled and MAY be provided. **Postconditions:** - Decision record appended with `decidedAt` and `reasonCode`. - Proposal transitions to `rejected` atomically. - Event `pcx.match-decision.recorded.v1` emitted. Event `pcx.match-proposal.rejected.v1` emitted on the proposal. ### `withdraw` A `withdraw` decision retracts a previously recorded `accept` decision from the same organization, provided the proposal has not yet been committed (i.e. is still `open`). > **Critical concurrency rule:** A `withdraw` decision MUST NOT reverse a proposal that has already reached `accepted` state. If the proposal is `accepted` by the time the `withdraw` is processed, the server MUST return `422 Unprocessable Entity` with `invalid-state-transition`. A committed match cannot be unwound by a delayed withdrawal. **Preconditions:** - **Authorization:** Actor belongs to `organizationId`. - **Scope required:** `matches:decide` - **Proposal state:** `MatchProposal` MUST still be in `open` state. If `accepted`, reject with `422 invalid-state-transition`. - **Prior decision:** An `accept` decision from the same `organizationId` MUST exist on this proposal. If not, return `422`. - **Version check:** `observedProposalVersion` MUST match current `resourceVersion`. **Postconditions:** - Withdrawal decision record appended (the prior `accept` record is NOT deleted; the log is append-only). - If policy was previously satisfied, the policy-satisfaction state is re-evaluated; proposal status remains `open` unless another `accept` decision now meets the threshold without this withdrawal. - Event `pcx.match-decision.recorded.v1` emitted. ## 3. Decision Record Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `id` | string (UUID) | yes | Stable immutable identifier for this decision record. | | `proposalId` | string | yes | Reference to the `MatchProposal`. | | `organizationId` | string | yes | The deciding organization. | | `decision` | enum (`accept`, `reject`, `withdraw`) | yes | The decision type. | | `decidedAt` | RFC 3339 timestamp | yes | When the decision was recorded (server-assigned). | | `decidedBy` | actor reference | yes | The specific user or system acting on behalf of the organization. | | `reasonCode` | string (vocabulary TBD) | no | Structured reason code; REQUIRED for `reject`. | | `comment` | string | no | Human-readable comment; access-controlled, not exposed to all parties. | | `observedProposalVersion` | integer | yes | The `resourceVersion` of the proposal at decision time. | ## 4. Invariants 1. **Append-only:** No `MatchDecision` record MUST be mutated or deleted after recording. 2. **Stale-version guard:** The server MUST reject any decision where `observedProposalVersion` does not match the current `resourceVersion` of the proposal. 3. **No post-commit withdrawal:** A `withdraw` decision MUST be rejected if the proposal is already `accepted`. A committed match is irrevocable through the decision API; it requires a separate cancellation flow on the `ExecutionReference`. 4. **Policy evaluation atomicity:** The check of whether all required decisions meet `requiredDecisionPolicy` MUST be performed within the same transaction as the decision append. Double-firing of the `accept` transition is prohibited. 5. **Comment access control:** The `comment` field MUST only be returned to the organization that authored it or to authorized protocol operators. It MUST NOT be disclosed to counterparty organizations. 6. **Single decision per org per proposal (accept/reject):** Each organization MUST record at most one non-withdrawn `accept` or `reject` on a given `MatchProposal`. Duplicate detection uses the `organizationId` + `proposalId` combination. --- # MatchProposal — Lifecycle State Machine Source: specification/state-machines/match-proposal.md **RFC 2119 normative language applies throughout.** ## 1. States | State | Meaning | |-------|---------| | `open` | Proposal created and active; awaiting decisions from required parties. | | `accepted` | All required `MatchDecision` records agree to accept; the match is committed. | | `rejected` | At least one required party has rejected the proposal; the match will not proceed. | | `expired` | `validUntil` passed before a conclusive decision was reached. | | `superseded` | A newer proposal has been created for the same offer/request pair; this proposal is no longer actionable. | | `withdrawn` | The broker or an authorized party has retracted the proposal before a decision. | **Terminal states:** `accepted`, `rejected`, `expired`, `superseded`, `withdrawn` ## 2. State-Transition Table | From State | Command | To State | Notes | |------------|---------|----------|-------| | *(none)* | Broker creates proposal *(system)* | `open` | Broker records a compatible offer/request pair. Proposal is immutable once created. | | `open` | `accept` *(triggered by MatchDecision records meeting policy)* | `accepted` | All required decisions recorded; policy satisfied. Atomic. | | `open` | `reject` *(triggered by any rejecting MatchDecision)* | `rejected` | Any required party rejects; match cannot proceed. | | `open` | `expire` *(system, clock)* | `expired` | `validUntil` reached with no conclusive decision. | | `open` | `supersede` *(system, on new proposal for same pair)* | `superseded` | A newer proposal is created; existing open proposal is closed. | | `open` | `withdraw` *(broker or authorized party)* | `withdrawn` | Broker retracts the proposal before decision. | ### Invalid Transitions Any command applied to a terminal state MUST return `422 Unprocessable Entity` with error type `urn:freyt:protocol:error:invalid-state-transition`. A `MatchProposal` is immutable after creation; its fields MUST NOT be updated. A newer rationale requires a new proposal, transitioning this one to `superseded`. ## 3. Preconditions ### Proposal creation (open) - **Authorization:** Broker system only. - **Scope required:** Broker holds `matches:read` and `capacity:read` + `loads:read` on behalf of both sides. - **Relationship state:** `PartnerRelationship` between the carrier and shipper organizations MUST be `active`. - **Version check:** `capacityOfferId` + observed offer `resourceVersion` AND `loadRequestId` + observed request `resourceVersion` MUST be recorded on the proposal at creation time. - **Policy check:** `requiredDecisionPolicy` MUST be set (e.g. `both-accept`, `carrier-only`, `shipper-only`). `validUntil` MUST be set and MUST be in the future. The proposal MUST NOT disclose protected fields from either resource. ### `accept` (open → accepted) - **Authorization:** Broker system, triggered when the count and roles of `MatchDecision` records with `decision: accept` satisfies `requiredDecisionPolicy`. - **Version check:** The `observedProposalVersion` on each `MatchDecision` MUST match the current `resourceVersion` of the proposal. A stale decision returns `409 Conflict` with `stale-resource-version`; the server MUST NOT silently apply a decision made against a changed proposal. - **Policy check:** All required organizations for the given `requiredDecisionPolicy` MUST have an `accept` decision recorded. - **Atomicity:** The transition to `accepted` and the corresponding transitions on `CapacityOffer` and `LoadRequest` MUST commit atomically. ### `reject` (open → rejected) - **Authorization:** Triggered by any `MatchDecision` record with `decision: reject` from a required party. - **Version check:** `observedProposalVersion` MUST match current `resourceVersion`. - **Policy check:** Rejection by a single required party is sufficient unless policy requires unanimous rejection. ### `expire` (system command) - **Authorization:** System clock process. - **Precondition:** Current time ≥ `validUntil` and `status` is still `open`. ### `supersede` (system command) - **Authorization:** Broker system only. - **Precondition:** A new `MatchProposal` referencing the same `(capacityOfferId, loadRequestId)` pair is being created. All existing `open` proposals for that pair MUST be transitioned to `superseded` atomically with the creation of the new proposal. ### `withdraw` (open → withdrawn) - **Authorization:** Broker system or an authorized protocol operator. - **Scope required:** `matches:read` + operator flag. - **Version check:** `If-Match` MUST match current `resourceVersion`. - **Policy check:** Withdrawal reason MUST be provided. ## 4. Postconditions | Command | Postconditions | |---------|---------------| | Proposal creation | `status` = `open`; `resourceVersion` = 1; `createdAt` recorded; `capacityOfferId` + version + `loadRequestId` + version recorded; event `pcx.match-proposal.created.v1` emitted. | | `accept` | `status` = `accepted`; `acceptedAt` recorded; `resourceVersion` incremented; `CapacityOffer` status updated to `matched` or `partiallyMatched`; `LoadRequest` status updated to `matched`; `ExecutionReference` record created; event `pcx.match-proposal.accepted.v1` emitted. | | `reject` | `status` = `rejected`; `rejectedAt` recorded; rejecting `organizationId` and `reasonCode` noted; `resourceVersion` incremented; event `pcx.match-proposal.rejected.v1` emitted. | | `expire` | `status` = `expired`; `expiredAt` recorded; `resourceVersion` incremented; linked `LoadRequest` may revert to `published` if no other proposals remain. | | `supersede` | `status` = `superseded`; `supersededAt` recorded; `supersededByProposalId` set; `resourceVersion` incremented. | | `withdraw` | `status` = `withdrawn`; `withdrawnAt` recorded; withdrawal reason stored; `resourceVersion` incremented. | ## 5. Concurrency Rules - **Accept and withdraw racing (critical rule):** `accept` and `withdraw` commands on the same proposal MUST be handled atomically within a single serializable transaction. Whichever acquires the exclusive lock first wins. **A committed `accepted` state MUST NOT be reversed by a delayed `withdraw` command.** If `withdraw` arrives after the proposal is `accepted`, it MUST return `422 Unprocessable Entity` with `invalid-state-transition`. - **Multiple decisions racing:** Two `MatchDecision` records from different organizations may arrive concurrently. Each decision MUST be appended independently. The broker evaluates whether the policy threshold is met only after both are durably stored. The policy-satisfaction check MUST be serialized to prevent double-firing of the `accept` transition. - **Supersede and accept racing:** If a new proposal creation (which would supersede the current one) races with an `accept` trigger, the accept MUST take precedence if it commits first. A supersede MUST NOT overwrite `accepted`. - **Expire and accept racing:** If `validUntil` is reached at the same moment a decision is committed, the accept transition takes precedence if it commits within the same transaction boundary; otherwise `expired` wins. - All proposal state transitions MUST be applied atomically. ## 6. Invariants 1. **Immutability after creation:** A `MatchProposal`'s fields (offer/request versions, compatibility data, rationale, policy) MUST NOT be modified after `status` is `open`. A correction requires a new proposal. 2. **Version snapshot integrity:** `observedCapacityOfferVersion` and `observedLoadRequestVersion` MUST be recorded on creation and MUST match the versions at creation time. 3. **No protected field disclosure:** The proposal MUST NOT surface protected fields (customer identity, exact address, price, complete goods details) unless the field-disclosure policy explicitly permits it. 4. **Decision policy completeness:** `requiredDecisionPolicy` MUST be set before a proposal is created and MUST NOT be changed. 5. **Terminal state finality:** No transition out of `accepted`, `rejected`, `expired`, `superseded`, or `withdrawn` is permitted. 6. **Cascading consistency:** When a proposal enters `accepted`, the linked `CapacityOffer` and `LoadRequest` MUST be updated in the same atomic operation. --- # PartnerRelationship — Lifecycle State Machine Source: specification/state-machines/partner-relationship.md **RFC 2119 normative language applies throughout.** ## 1. States | State | Meaning | |-------|---------| | `draft` | Relationship request assembled but not yet submitted to the accepting organization. Visible only to the requesting organization. | | `requested` | Submission sent; awaiting acceptance by the accepting organization. Both organizations can observe the relationship. | | `active` | Both sides have authorized the relationship. Capacity and load data sharing is enabled within the granted scopes. | | `suspended` | Temporarily deactivated. Data sharing is disabled; existing matched offers and requests are unaffected but no new sharing occurs. | | `terminated` | Permanently closed. Data sharing has ended. The record is retained for audit. | **Terminal states:** `terminated` ## 2. State-Transition Table | From State | Command | To State | Notes | |------------|---------|----------|-------| | *(none)* | `POST /partner-relationships` | `draft` | Initiating organization creates the relationship object. | | `draft` | `submit` (implicit on first `POST /partner-relationships/{id}/request`) | `requested` | Requesting organization submits to accepting org. | | `requested` | `accept` (`POST /partner-relationships/{id}/accept`) | `active` | Accepting organization approves. | | `requested` | `reject` (`POST /partner-relationships/{id}/reject`) | `terminated` | Accepting organization declines. | | `requested` | `withdraw` | `terminated` | Requesting organization withdraws before acceptance. | | `active` | `suspend` (`POST /partner-relationships/{id}/suspend`) | `suspended` | Either authorized organization suspends. | | `active` | `terminate` (`POST /partner-relationships/{id}/terminate`) | `terminated` | Either authorized organization terminates. | | `suspended` | `reactivate` (`POST /partner-relationships/{id}/reactivate`) | `active` | The organization that suspended it reactivates. | | `suspended` | `terminate` | `terminated` | Either authorized organization terminates while suspended. | ### Invalid Transitions Any command applied to a state not listed in the "From State" column above MUST return `422 Unprocessable Entity` with error type `urn:freyt:protocol:error:invalid-state-transition`. ## 3. Preconditions ### `submit` (draft → requested) - **Authorization:** Actor belongs to `requestingOrganizationId`. - **Scope required:** `relationships:manage` - **Relationship state:** N/A (creating new). - **Version check:** None required for initial submission. - **Policy check:** `acceptingOrganizationId` must be a known organization in the registry. ### `accept` (requested → active) - **Authorization:** Actor belongs to `acceptingOrganizationId`. - **Scope required:** `relationships:manage` - **Version check:** `If-Match` header MUST match current `resourceVersion`. - **Policy check:** Accepting organization's policy must not prohibit the requested scopes. ### `reject` (requested → terminated) - **Authorization:** Actor belongs to `acceptingOrganizationId`. - **Scope required:** `relationships:manage` - **Version check:** `If-Match` header MUST match current `resourceVersion`. ### `withdraw` (requested → terminated) - **Authorization:** Actor belongs to `requestingOrganizationId`. - **Scope required:** `relationships:manage` - **Version check:** `If-Match` header MUST match current `resourceVersion`. ### `suspend` (active → suspended) - **Authorization:** Actor belongs to either `requestingOrganizationId` or `acceptingOrganizationId`. - **Scope required:** `relationships:manage` - **Version check:** `If-Match` header MUST match current `resourceVersion`. - **Policy check:** Suspension reason MUST be provided. ### `terminate` (active | suspended → terminated) - **Authorization:** Actor belongs to either participating organization. - **Scope required:** `relationships:manage` - **Version check:** `If-Match` header MUST match current `resourceVersion`. - **Policy check:** Termination reason MUST be provided. ### `reactivate` (suspended → active) - **Authorization:** Actor belongs to the organization that issued the `suspend` command. - **Scope required:** `relationships:manage` - **Version check:** `If-Match` header MUST match current `resourceVersion`. - **Policy check:** If `effectiveUntil` has passed, reactivation MUST be rejected. ## 4. Postconditions | Command | Postconditions | |---------|---------------| | `submit` | `status` set to `requested`; `submittedAt` timestamp recorded; event `pcx.partner-relationship.requested.v1` emitted. | | `accept` | `status` set to `active`; `effectiveFrom` set to current time if not pre-set; `resourceVersion` incremented; event `pcx.partner-relationship.activated.v1` emitted. | | `reject` | `status` set to `terminated`; `terminatedAt` recorded; rejection reason stored; event `pcx.partner-relationship.terminated.v1` emitted. | | `withdraw` | `status` set to `terminated`; `terminatedAt` recorded; withdrawal reason stored; event emitted. | | `suspend` | `status` set to `suspended`; `suspendedAt` recorded; suspension reason stored; `resourceVersion` incremented; event `pcx.partner-relationship.suspended.v1` emitted. Data sharing ceases immediately. | | `terminate` | `status` set to `terminated`; `terminatedAt` recorded; `resourceVersion` incremented; event `pcx.partner-relationship.terminated.v1` emitted. Data sharing ceases immediately. | | `reactivate` | `status` set to `active`; `reactivatedAt` recorded; `resourceVersion` incremented; event `pcx.partner-relationship.reactivated.v1` emitted. | ## 5. Concurrency Rules - All state-changing commands require `If-Match` with the current ETag. - Concurrent `suspend` and `terminate` on the same resource: the first writer wins; the second receives `409 Conflict` with `stale-resource-version`. - Concurrent `accept` and `withdraw`: whichever commits first wins; the other receives `409`. - The server MUST apply transitions atomically. A transition MUST NOT be partially committed. ## 6. Invariants 1. **Uniqueness:** At most one non-terminated `PartnerRelationship` may exist between a given `(requestingOrganizationId, acceptingOrganizationId)` pair at any time. 2. **Audit retention:** Records in `terminated` state MUST be retained for the period defined in `retentionPolicyRef` and MUST NOT be physically deleted. 3. **Scope containment:** `scopes` in an `active` relationship must be a subset of scopes granted by both organizations' policies. 4. **Effective period:** If `effectiveUntil` is set and the current time exceeds it, the relationship behaves as `suspended` for authorization checks even if `status` is `active`. A background job transitions it to `terminated`. 5. **No resurrection:** A `terminated` relationship MUST NOT be transitioned to any other state. A new relationship may be created to replace it. 6. **Visibility isolation:** A `draft` relationship MUST NOT be visible to the `acceptingOrganizationId` until `status` is `requested`. --- # State Machines Source: specification/state-machines/README.md Lifecycle state-transition specifications for OpenFreytProtocol resources. Each file in this directory describes one resource lifecycle: - permitted states and their meanings - valid transitions (command → preconditions → new state) - terminal states - invalid transition handling - concurrency and idempotency behaviour ## Required (foundation gate §13) - [ ] `offer-lifecycle.md` — capacity offer states and transitions - [ ] `request-lifecycle.md` — load request states and transitions - [ ] `proposal-lifecycle.md` — match proposal states and transitions - [ ] `relationship-lifecycle.md` — partner relationship states and transitions - [ ] `execution-lifecycle.md` — execution record states and transitions State machines here feed directly into: - `requirements/invariants.yaml` — formal safety invariants - `formal/tla/` — TLA+ models - domain unit tests --- # Release Evidence Source: verification/releases/README.md Release evidence artifacts for OpenFreytProtocol. ## Purpose (QUALITY-STRATEGY.md §9, §10) Every release is backed by durable, archived evidence. A release is blocked unless current evidence exists for every critical requirement. ## Evidence format (to be specified) Each release evidence bundle contains: - requirement coverage report (all critical requirements with test references) - invariant coverage report - conformance suite results (all implementations) - security scan results - performance test results - compatibility test results - SBOM (software bill of materials) - signed release artifacts ## Retention Evidence is immutable after archival. Corrections produce new records referencing prior ones. --- # Standards assessment for OpenFreytProtocol Source: docs/research/2026-08-31-standards-assessment.md **Date:** 31 August 2026 **Decision:** No existing logistics standard becomes the canonical protocol. OpenFreytProtocol uses a small project-owned contract built on mature internet standards. ## Selection criteria A dependency should be used only when it is: 1. publicly and easily accessible; 2. implementable without membership or vendor permission; 3. mature enough for production use; 4. supported by practical tooling; 5. appropriately scoped; 6. compatible with rapid additive iteration; 7. independent from one TMS or marketplace vendor. “Open” means an openly available specification and usable implementation contract, not merely an open-source SDK around a proprietary service. ## Comparison | Candidate | Accessibility | Maturity | Road-capacity matching fit | Iteration speed | Disposition | |---|---:|---:|---:|---:|---| | Project-owned JSON/OpenAPI contract | 5/5 | Built on mature primitives | 5/5 | 5/5 | Canonical | | OASIS UBL 2.4 | 5/5 | 5/5 | 2/5 | 2/5 | Optional document adapter | | UN/CEFACT multimodal models | 4/5 | 5/5 | 3/5 | 1/5 | Terminology/code-list reference only | | UN/EDIFACT | 4/5 | 5/5 | 2/5 | 1/5 | Legacy TMS adapter | | GS1 EPCIS 2.0 | 5/5 | 4/5 | 1/5 matching; 4/5 visibility | 3/5 | Optional execution-evidence adapter | | DCSA APIs | 5/5 | 4/5 | 1/5 for road exchange | 3/5 | Borrow API/governance patterns only | | iSHARE/BDI | Public concepts; operational adoption is complex | Mixed by component | Trust layer, not exchange semantics | 1/5 | Principles only; no pilot dependency | | eFTI/e-CMR | Sector/regulation oriented | Evolving/segment-specific | Documents, not matching | 1–2/5 | Future compliance adapter | | Proprietary freight exchanges | Often restricted | Vendor-specific | Often functionally strong | 2/5 | Never canonical; avoid closed dependency | Scores are architecture judgments for this use case, not claims of general standard quality. ## Candidate notes ### OASIS UBL 2.4 UBL is a mature public standard for business documents, including transport-related documents. Its breadth and document orientation create substantial mapping overhead and do not naturally represent transient capacity offers, bilateral visibility and match negotiation. **Use when:** a participant already exchanges UBL documents. **Do not use for:** the internal broker model or minimum integration contract. Official specification: https://docs.oasis-open.org/ubl/UBL-2.4.html ### UN/CEFACT UN/CEFACT provides internationally established semantics and code lists for trade and multimodal transport. The models are broad and less convenient for a small real-time API. **Use when:** selecting stable terms or UN code lists. **Do not use for:** implementing the entire reference data model. Official standards overview: https://unece.org/trade/uncefact/mainstandards ### UN/EDIFACT EDIFACT has deep deployment in established logistics environments and can be required by existing TMS installations. Partner-specific message profiles and legacy tooling make it expensive as a greenfield canonical protocol. **Use when:** onboarding a participant with an existing IFTMIN/IFTSTA integration. **Do not use for:** native PCX clients. ### GS1 EPCIS 2.0 EPCIS is a ratified standard for exchanging supply-chain visibility events across enterprises. It answers what happened, where, when and to which object; it does not define offers, demand, partner negotiation or match commitment. **Use when:** importing execution/visibility evidence. **Do not use for:** capacity discovery and matching. Official specification: https://ref.gs1.org/standards/epcis/ ### DCSA DCSA demonstrates modern API governance and event patterns but is centered on container ocean shipping. Its domain semantics should not be transplanted into road-capacity exchange. **Use when:** learning from API design, versioning and conformance practices. **Do not use for:** PCX domain resources. ### iSHARE and BDI These initiatives address trust, authorization, data sovereignty and federated logistics data sharing rather than the business payload required for matching capacity. **Use when:** evaluating later federation or regulated trust requirements. **Do not use for:** blocking the initial broker or requiring consortium-wide certification. ### eFTI and e-CMR These concern regulatory freight information and transport documentation. They are adjacent to execution but not a marketplace protocol. **Use when:** linking accepted execution to compliant documents. **Do not use for:** publishing and matching capacity. ## Selected mature primitives | Concern | Selection | |---|---| | Resource schemas | JSON Schema 2020-12 | | HTTP API | OpenAPI 3.1 | | Event envelope | CloudEvents | | Event-channel documentation | AsyncAPI, optional | | Authentication | OAuth 2.0 Client Credentials | | Interactive identity | OpenID Connect where needed | | HTTP errors | RFC 9457 Problem Details | | Time | RFC 3339 and IANA time-zone names | | Geography | GeoJSON RFC 7946 | | Countries | ISO 3166-1 | | Currency | ISO 4217 | | Logistics locations | UN/LOCODE | | Units | UNECE Recommendation 20/21 | | Identifiers | UUIDv7 / RFC 9562 | ## Resulting strategy OpenFreytProtocol owns only the narrow semantics needed for partner authorization, capacity offers, load requests, proposals, decisions and impact evidence. Everything else is either: - a mature primitive; - an opaque external reference; - a versioned extension; or - an optional boundary adapter justified by a real participant. This avoids both Freyt lock-in and standards-driven overengineering. --- # Partner Capacity Exchange design research Source: docs/research/partner-capacity-exchange-design.md **Status:** Curated design input; ADRs and the normative specification take precedence. ## 1. Network model A closed partner network is the preferred initial operating model. Organizations establish bilateral relationships before discovering each other's capacity or demand. This provides a practical basis for authorization, commercial trust and controlled data disclosure without turning the protocol into a public freight marketplace. The network can later support groups or communities, but group visibility must resolve to explicit organization-level authorization. ## 2. Initial exchange opportunities The strongest initial patterns are: - empty return capacity; - residual weight, volume, pallet-space or loading-metre capacity; - recurring lanes; - corridor-compatible load requests; - vehicle- and time-window-compatible partner handoff. These patterns can be represented without synchronizing a participant's complete planning or order domain. ## 3. Exchange lifecycle ```text Partner relationship requested → relationship accepted → capacity offer / load request published → compatible opportunity discovered → non-binding proposal created → authorized decisions recorded → accepted exchange linked to execution → completed / cancelled / disputed outcome ``` Withdrawal, expiry, rejection and cancellation are first-class outcomes. They are not exceptional database deletions. ## 4. Disclosure stages ### Before an active relationship Share only organization-discovery information needed to request a relationship. ### Active relationship, before a proposal Depending on policy, share coarse regions, time windows, vehicle/body type, available or required capacity and operational constraints. ### Open proposal Share enough compatibility and rationale data to make an informed decision. Exact addresses, customers, pricing and sensitive goods information remain policy-controlled. ### Accepted proposal Release the operational details explicitly allowed by the relationship and decision policy. Participants retain their own authoritative order and trip records. ## 5. Matching Initial matching should be deterministic and explainable. A proposal records which checks passed, failed or were not evaluated. Potential checks: - relationship authorization; - resource availability and expiry; - area/corridor overlap; - pickup and delivery windows; - vehicle/body compatibility; - capacity dimensions; - equipment and handling requirements; - goods restrictions; - maximum detour or participant policy. A matcher abstains when required data is missing or ambiguous. It never converts uncertainty into an operational commitment. ## 6. Marketplace risks and protocol controls | Risk | Protocol control | |---|---| | Unauthorized commercial visibility | Bilateral relationships, scopes and hidden-resource responses | | Stale capacity | Expiry, version checks and withdrawal commands | | Double commitment | Explicit lifecycle, optimistic concurrency and idempotency | | Race between acceptance and withdrawal | Atomic command handling and formal state invariants | | Duplicate integration messages | Idempotency keys and event IDs | | Out-of-order updates | Monotonic resource versions | | Excessive disclosure | Field-level disclosure phases | | Lock-in | Public schemas, conformance tests and independent client | | Semantic drift | Versioned definitions and compatibility checks | | Adapter corruption | Quarantine, diagnostics and mapping tests | ## 7. Central broker versus federation A central reference broker is appropriate for the first implementation because it simplifies consistency, onboarding, authorization and observability. The public protocol prevents the broker from becoming a permanent proprietary boundary. Later federation remains possible if real operational requirements justify the additional identity, discovery, consistency and governance complexity. Federation should not be implemented pre-emptively. ## 8. Commercial boundary The core protocol does not standardize: - pricing strategy or market-price formation; - payment or escrow; - liability allocation; - claims; - external legal contract terms; - partner reputation scores. A tender may disclose a price or stable commercial-terms reference when bilateral policy permits it. These fields record the terms presented for a protocol decision; they do not define payment behavior or claim that the protocol commitment replaces an external legal transport contract. A match proposal remains non-binding. ## 9. Interoperability conclusion The protocol should own only its exchange-specific semantics and build on mature internet primitives. Existing TMS or document formats belong behind adapters. An external concept is promoted into the core only after multiple independent participants demonstrate the same need and conformance tests can define its behaviour. ## 10. Discovery and execution transfer are separate A private marketplace needs two connected but distinct loops: 1. discovery compares capacity and demand and produces non-binding proposals; 2. tendering freezes proposed terms, records required decisions and creates an immutable commitment; 3. execution handover releases the minimum operational package and waits for destination acknowledgement; 4. execution updates and outcomes close the shared loop. A connected system's internal ride, order, shipment or route is not portable. The transferable object is a bounded movement with stops, time windows, cargo facts, equipment constraints, references and disclosure-controlled instructions. The source and executing systems create or retain their own local records. ## 11. Multi-system trust Trust has three independent dimensions: - **organizational trust:** the participant has a verified broker identity and accepted exchange terms; - **technical trust:** each connector is registered, organization-bound, versioned, revocable and conformant; - **bilateral trust:** an active relationship permits specific products, actions, fields and lifecycle behavior. An access token proves a technical caller identity. It does not by itself grant partner visibility. Authorization also evaluates relationship policy, resource visibility, disclosure phase, actor role and lifecycle state. ## 12. Handover reliability Commercial acceptance and technical import are different facts. A tender may be accepted while the destination connector is offline or rejects an unsafe mapping. The protocol therefore needs a handover acknowledgement with `imported`, `linked`, `rejected` and `quarantined` outcomes. At-least-once event delivery requires durable inbox and outbox state, idempotency, external-reference mapping and reconciliation from authoritative resource versions. Duplicate handovers must resolve to one destination record. Missed or reordered events must not regress state. ## 13. Connector boundary A connector maps between a local model and canonical exchange resources. It preserves source and destination references, validates before publishing, quarantines ambiguity instead of inventing data, exposes health and mapping failures, and reconciles after downtime. Direct writes to an application's internal database are unsuitable unless that application explicitly exposes the database interface as a supported integration contract. Product-specific APIs, files, messages and schemas remain adapter concerns and do not change the canonical protocol. --- # Roadmap Source: ROADMAP.md **Status:** Proposed protocol delivery sequence **Objective:** Deliver a rigorously specified, TMS-independent private exchange for discovery, tendering, commitment and execution handover with independently verifiable interoperability. ## Foundation gate — Assurance before production code No production broker or SDK implementation begins until the following exist: - accepted quality charter; - requirement and invariant registries; - glossary; - lifecycle state machines; - layered authorization matrix and bilateral policy model; - organization verification and connector-registration model; - OAuth/OIDC and event-security profiles; - threat model and abuse cases; - JSON Schema and OpenAPI validation pipeline; - conformance-test skeleton; - compatibility policy; - release-evidence format; - tracked ADRs for normative architecture decisions. ## Phase 1 — Normative core specification ### Scope Specify: - `OrganizationReference`; - `PartnerRelationship`; - `CapacityOffer`; - `LoadRequest`; - `MatchProposal`; - `MatchDecision`; - `TransportTender` and counterproposals; - `TransportCommitment`; - `ExecutionHandover` and `HandoverAcknowledgement`; - common identifiers, timestamps, units and external references; - lifecycle transitions; - authorization and disclosure rules; - errors, idempotency and concurrency; - CloudEvents envelope and event names. ### Deliverables - requirement IDs and traceability graph; - normative JSON Schemas; - OpenAPI 3.1 contract; - state-transition tables; - valid and invalid examples; - initial formal lifecycle model; - executable schema and transition checks. ### Exit criteria - every normative clause maps to a requirement and test; - an independent engineer can validate examples without Freyt code; - illegal transitions and unauthorized visibility have executable negative tests; - no public contract exposes Freyt-internal types. ## Phase 2 — Conformance suite and reference model ### Scope - pure deterministic reference model; - provider conformance profile; - consumer conformance profile; - broker conformance profile; - property-based command sequence generation; - model-based expected-state comparison; - compatibility diff tooling; - mutation testing for critical rules. ### Exit criteria - conformance tests run without importing an implementation; - randomized sequences are reproducible by seed; - all lifecycle and authorization invariants are covered; - no unexplained mutation survives in critical protocol logic. ## Phase 3 — Smallest end-to-end implementation ### Scope - relationship activation; - one-off capacity offer; - one-off load request; - deterministic match proposal; - explicit accept/reject; - withdrawal and expiry; - immutable decision history; - reference broker HTTP API; - Freyt reference client; - independent simulator or second client. ### Exit criteria - two organizations complete an exchange using only public contracts; - unauthorized parties cannot discover resources; - retries cannot create duplicate resources or decisions; - stale decisions are rejected; - no commitment exists without the configured acceptance policy; - both clients pass the same black-box suite. ## Phase 4 — Events, execution handover and recovery ### Scope - CloudEvents publication; - authenticated and replay-resistant webhook or message-channel delivery profile; - `ExecutionHandover`, `HandoverAcknowledgement`, `ExecutionReference`, `ExecutionUpdate` and `ExecutionOutcome`; - source and destination external-reference mapping; - material amendments, cancellation and exception flows; - transactional outbox/inbox; - duplicate and out-of-order delivery handling; - connector capability negotiation and cursor-based reconciliation; - quarantine and dead-letter recovery; - restart recovery; - structured logs, traces and operational metrics. ### Exit criteria - commit/response timeouts are retry-safe; - duplicate and reordered events converge to correct state; - broker restart cannot lose accepted state; - duplicate handovers create or link exactly one destination record; - commercial commitment and destination import acknowledgement remain independently observable; - execution records remain linked without importing complete connected-system models; - missed delivery converges through reconciliation; - quarantine, dead letters and other failure modes are observable and actionable. ## Phase 5 — Recurring capacity ### Scope - recurring offers and requests; - calendars and exceptions; - corridor profiles; - occurrence-specific withdrawal; - proposal generation across recurrence windows. ### Exit criteria - recurrence expansion is deterministic; - time-zone and daylight-saving boundaries are verified; - exceptions cannot resurrect withdrawn occurrences; - bounded generation prevents unbounded resource growth. ## Phase 6 — Demand-led adapters Build an adapter only for a confirmed participating system. Potential adapters: - generic CSV import/export; - lightweight web portal; - participant-specific REST API; - established EDI or business-document formats; - execution-status or visibility feeds. ### Exit criteria - mappings preserve core semantics; - lossy mappings are documented; - invalid input is quarantined with actionable diagnostics; - adapter-specific fields do not leak into the canonical model; - conformance and organization-isolation tests pass. ## Phase 7 — Stable protocol release ### Required evidence - complete requirements traceability; - formal invariant checks; - full mutation and property-test results; - independent-client interoperability; - backward-compatibility results; - migration and recovery rehearsal; - current threat model; - security assessment with no unresolved critical/high finding; - sustained load and failure-injection results; - signed schemas, artifacts and SBOM; - archived release evidence. ## Deferred capabilities These require separate requirements and architecture decisions: - public marketplace discovery; - bidding and price discovery; - payments, escrow and claims; - automatic legal contracting; - autonomous award; - multi-leg and cross-dock optimization; - decentralized brokers; - federated trust infrastructure; - machine-learned matching; - reputation scoring; - commercial network analytics. ## Immediate next actions 1. Establish repository license and governance. 2. Create `requirements/requirements.yaml` and `requirements/invariants.yaml`. 3. Freeze the first glossary and lifecycle state machines. 4. Write the authorization matrix and threat model. 5. Create valid and invalid examples for the five core exchange resources. 6. Implement schema, traceability and compatibility checks in CI. 7. Build the reference model and conformance harness before the broker. --- # Partner Capacity Exchange Protocol Source: PROTOCOL.md **Working name:** OpenFreytProtocol / PCX **Status:** Pre-alpha design, not an implementation contract **Language:** Protocol names and schemas use English ## 1. Protocol objective PCX provides the smallest interoperable contract required for mutually accepted logistics partners to discover compatible capacity and demand, tender transport work, create explicit commitments, transfer a bounded execution package and prove the operational outcome. It is a full private exchange protocol, not a universal transport model. It deliberately avoids copying complete vendor-native orders, shipments, rides, routes, invoices, vehicles, customers or contracts. Connected systems retain their local records and link them through protocol identifiers and namespaced external references. The normative execution-transfer behavior is defined by `specification/private-exchange-profile.md`. ## 2. Normative technology profile | Layer | Choice | |---|---| | Encoding | UTF-8 JSON | | Payload semantics | JSON Schema 2020-12 | | Synchronous transport | HTTPS using OpenAPI 3.1 | | Event envelope | CloudEvents 1.x | | Event channel description | AsyncAPI, optional | | Authentication | OAuth 2.0 Client Credentials; OIDC for interactive users where applicable | | Error payloads | RFC 9457 Problem Details | | Time | RFC 3339 UTC timestamps; IANA time-zone identifier when local-time interpretation matters | | Geometry | GeoJSON RFC 7946 | | Countries | ISO 3166-1 alpha-2 | | Currency | ISO 4217 | | Locations | UN/LOCODE where available | | Units | UNECE Recommendation 20/21 codes | | Resource IDs | UUIDv7 preferred; opaque to consumers | JSON Schema is the source of truth for shared payload structures. OpenAPI is normative for HTTP paths, authorization, status codes and headers, and references the schemas rather than redefining them. ## 3. Core invariants 1. Every resource belongs to exactly one owning organization. 2. Visibility requires an active bilateral `PartnerRelationship` and matching scope. 3. An offer or request is not a legal or operational commitment. 4. A proposal is explainable and non-binding. 5. An award or commitment requires explicit authorized acceptance according to the configured decision policy. 6. Retried commands are idempotent. 7. Events can be delivered more than once; consumers deduplicate by event ID. 8. Resources carry a monotonically increasing `resourceVersion`. 9. State-changing events preserve causal order per resource. 10. Historical decisions and execution outcomes cannot be silently overwritten. 11. Exact addresses and customer identity are not required before acceptance. 12. The public contract does not contain Freyt-specific identifiers or implementation details. 13. Commercial acceptance and technical handover acknowledgement are separate facts. 14. Duplicate handover delivery cannot create duplicate destination work. 15. Accepted decision-relevant terms cannot change without a linked revision and required renewed acceptance. 16. Payload fields cannot override the authenticated connector's organization binding. 17. Connected systems retain their local records; the exchange transfers only the bounded canonical execution package. ## 4. Common envelope Every resource includes: ```json { "id": "0195d8d2-18c7-7b18-9b4e-06d26469c821", "resourceVersion": 1, "ownerOrganizationId": "org_carrier_a", "createdAt": "2026-09-15T08:00:00Z", "updatedAt": "2026-09-15T08:00:00Z", "externalReferences": [ { "system": "carrier-a-tms", "type": "trip", "value": "TRIP-4812" } ] } ``` External references are namespaced and never treated as globally unique without `system` and `type`. ## 5. Resources ### 5.1 PartnerRelationship Represents bilateral authorization between two organizations. Minimum fields: - `id` - `requestingOrganizationId` - `acceptingOrganizationId` - `status`: `pending | active | suspended | terminated` - `scopes`: allowed resource/action combinations - `visibilityPolicy` - `effectiveFrom` - `effectiveUntil`, optional - `retentionPolicyRef`, optional A relationship becomes `active` only after authorized acceptance. Termination stops future sharing but does not erase required audit history. ### 5.2 CapacityOffer Represents available vehicle or trip capacity. Minimum fields: - common envelope; - `status`: `draft | published | partiallyMatched | matched | withdrawn | expired | cancelled`; - `availabilityWindow`; - `originArea`; - `destinationArea`; - `direction`, optional when areas imply direction; - `vehicleProfile`; - `availableCapacity`; - `constraints`; - `maximumDetour`, optional; - `expiresAt`; - `visibility`: explicit partner IDs or a relationship-defined group. Capacity dimensions are explicit and independently optional: ```json { "availableCapacity": { "weight": { "value": 12000, "unitCode": "KGM" }, "volume": { "value": 32, "unitCode": "MTQ" }, "loadingMetres": { "value": 7.2, "unitCode": "MTR" }, "palletSpaces": 18 } } ``` A consumer must not infer an unspecified dimension. ### 5.3 LoadRequest Represents demand for transport capacity, not a complete commercial order. Minimum fields: - common envelope; - `status`: `draft | published | proposed | matched | withdrawn | expired | cancelled`; - `pickupArea` and `pickupWindow`; - `deliveryArea` and `deliveryWindow`; - `requiredCapacity`; - `vehicleRequirements`; - `handlingRequirements`, optional; - `goodsProfile` with coarse, non-sensitive classification; - `expiresAt`; - `visibility`. Customer identity, exact address, price and complete goods details are excluded unless policy explicitly permits disclosure. ### 5.4 MatchProposal An immutable proposal connecting one compatible offer and request. A newer proposal supersedes rather than mutates an accepted rationale. Minimum fields: - `capacityOfferId` and observed version; - `loadRequestId` and observed version; - `status`: `open | accepted | rejected | expired | superseded | withdrawn`; - `validUntil`; - `compatibility` results; - `routeCompatibility`, including structured distance or detour estimates when available; - `capacityCompatibility`, including the dimensions used for comparison; - `rationale` as structured reason codes plus human-readable summary; - `requiredDecisionPolicy`; - `createdBy`: broker, participant or authorized planner. The proposal must never disclose protected fields merely because the broker found a potential match. ### 5.5 MatchDecision An append-only decision made by an authorized organization. Minimum fields: - `proposalId`; - `organizationId`; - `decision`: `accept | reject | withdraw`; - `decidedAt`; - `decidedBy` as actor reference; - `reasonCode`, optional; - `comment`, optional and access-controlled; - `observedProposalVersion`. The decision policy defines whether both organizations must accept and which role may decide. A stale version returns a conflict; the server does not silently apply the decision to a changed proposal. ### 5.6 TransportTender A versioned request for a selected partner to execute a bounded transport movement. It freezes the decision-relevant terms shown to the recipient and references the originating proposal where applicable. Minimum fields: - issuing and intended executing organizations; - movement scope: `fullTransport | routeLeg | collection | delivery`; - expiry and required decision policy; - cargo, equipment, stop and time-window requirements allowed at the current disclosure phase; - commercial terms or a stable external terms reference; - amendment and cancellation policy; - source external references. A counterproposal creates a new immutable revision. It never silently modifies terms already presented for decision. ### 5.7 TransportCommitment Immutable proof that every decision required by the tender policy was accepted before expiry or valid withdrawal. Minimum fields: - accepted tender revision; - required decisions and actors; - acceptance timestamps; - deterministic digest of the accepted decision-relevant payload; - relationship-policy version; - resulting capacity reservation or consumption. A commitment proves agreement. It does not prove that the executing system imported the work. ### 5.8 ExecutionHandover A disclosure-controlled operational package containing only the stops, time windows, cargo facts, equipment constraints, references, instructions, documents and contact details required to execute the committed movement. The source and executing systems retain their own local records. The handover links them through the commitment identifier and namespaced external references. ### 5.9 HandoverAcknowledgement Records whether the executing connector `imported`, `linked`, `rejected` or `quarantined` the handover. Successful acknowledgements include the destination external reference. Duplicate delivery must resolve to the same local record and acknowledgement. Commercial acceptance and technical handover acknowledgement are separate transitions. ### 5.10 ExecutionReference Links a commitment and handover to local execution records without copying a complete connected-system object. Minimum fields: - commitment and handover references; - participating organization references; - source and destination external references; - execution status; - planned and actual time range; - cancellation or exception reason where applicable. ### 5.11 ExecutionUpdate Communicates shared milestones and exceptions without replacing either participant's detailed local state. Baseline milestones are `assigned | atPickup | pickedUp | inTransit | delayed | atDelivery | delivered | failed | cancelled | disputed`. ### 5.12 ExecutionOutcome Records whether and how a commitment was executed without copying the participants' complete operational records. Minimum fields: - commitment, handover and execution reference; - `status`: `planned | inProgress | completed | cancelled | failed | disputed`; - planned and actual time range; - participant confirmations; - external execution references; - operational deviations or exception reason, where applicable; - optional capacity-utilization and route observations; - deduplication key; - correction/supersession reference. Execution outcomes are append-only. A correction points to the superseded record and includes a reason. ## 6. Lifecycle and commands Prefer intent-revealing commands over arbitrary resource patching: ```text POST /partner-relationships POST /partner-relationships/{id}/accept POST /partner-relationships/{id}/suspend POST /capacity-offers POST /capacity-offers/{id}/publish POST /capacity-offers/{id}/withdraw POST /load-requests POST /load-requests/{id}/publish POST /load-requests/{id}/withdraw GET /match-proposals POST /match-proposals/{id}/decisions POST /transport-tenders POST /transport-tenders/{id}/issue POST /transport-tenders/{id}/decisions POST /transport-tenders/{id}/counterproposals POST /transport-tenders/{id}/withdraw POST /transport-commitments/{id}/amendments POST /transport-commitments/{id}/cancel POST /execution-handovers POST /execution-handovers/{id}/acknowledgements POST /execution-references POST /execution-updates POST /execution-outcomes POST /execution-outcomes/{id}/corrections ``` The exact endpoint set remains to be specified in OpenAPI. Generic `PATCH` should be limited to non-decision draft data because it obscures lifecycle semantics and audit intent. ## 7. Idempotency and concurrency - Every create or command operation accepts `Idempotency-Key` scoped to the authenticated organization and operation. - The same key and identical payload returns the original outcome. - The same key with a different payload returns `409 Conflict`. - State-changing commands include `If-Match` or an explicit observed `resourceVersion`. - Stale writes return `409 Conflict` or `412 Precondition Failed` with an RFC 9457 body. - Idempotency records must outlive the maximum client retry window. ## 8. Events Example event types: ```text pcx.partner-relationship.activated.v1 pcx.capacity-offer.published.v1 pcx.capacity-offer.withdrawn.v1 pcx.load-request.published.v1 pcx.load-request.withdrawn.v1 pcx.match-proposal.created.v1 pcx.match-decision.recorded.v1 pcx.match-proposal.accepted.v1 pcx.transport-tender.issued.v1 pcx.transport-tender.countered.v1 pcx.transport-commitment.created.v1 pcx.transport-commitment.amended.v1 pcx.transport-commitment.cancelled.v1 pcx.execution-handover.offered.v1 pcx.execution-handover.acknowledged.v1 pcx.execution.updated.v1 pcx.execution.completed.v1 pcx.execution-outcome.recorded.v1 pcx.execution-outcome.corrected.v1 ``` CloudEvent requirements: - globally unique `id`; - `source` identifies the broker/producer; - `subject` identifies the resource; - `type` uses the versioned event name; - `time` uses RFC 3339; - `dataschema` points to an immutable schema version; - extension attributes include `organizationid`, `resourceversion`, `correlationid` and, where applicable, `causationid`. Delivery is at least once. Consumers must deduplicate and ignore an event with an older or equal applied resource version. Sensitive payload projection is audience-specific. ## 9. Authorization and privacy Authorization evaluates: 1. token validity and broker audience; 2. active connector or interactive user registration; 3. active organization; 4. resource ownership or participation; 5. active partner relationship; 6. granted client scope; 7. bilateral relationship policy; 8. resource visibility policy; 9. field disclosure phase; 10. actor role for decisions; 11. lifecycle preconditions; 12. resource version and expiry. API scope, bilateral policy and resource visibility are independent gates. Passing one does not bypass another. Connector credentials are bound to exactly one organization and environment; payload fields cannot override that binding. Machine connectors use OAuth 2.0 Client Credentials and should use `private_key_jwt` or mutual TLS in production. Interactive users use OIDC Authorization Code with PKCE. Internal connected-system session tokens are not portable exchange credentials unless an explicit federation or token-exchange profile validates their issuer, audience, organization and actor semantics. Recommended scopes: ```text relationships:manage capacity:publish capacity:read loads:publish loads:read matches:read matches:decide tenders:publish tenders:read tenders:decide commitments:read handovers:write handovers:read handovers:acknowledge execution:update outcomes:write outcomes:review connectors:manage ``` A `404` may be preferable to `403` when revealing resource existence would leak partner activity. Broker logs must redact exact locations, customer identity and goods details by default. ## 10. Error model Errors use RFC 9457 and stable protocol codes: ```json { "type": "https://openfreytprotocol.example/problems/stale-resource-version", "title": "Stale resource version", "status": 409, "code": "STALE_RESOURCE_VERSION", "detail": "The proposal changed after it was read.", "correlationId": "corr_019..." } ``` Initial codes should cover validation, authentication, authorization, hidden resource, inactive relationship, stale version, idempotency conflict, invalid transition, expired resource and incompatible decision policy. ## 11. Versioning - Protocol releases use semantic versioning. - JSON schemas have immutable versioned identifiers. - Additive optional fields and new event types are minor changes. - Changed meaning, removed fields or stricter existing validation require a major version. - Consumers ignore unknown optional fields but never unknown enum values in decision-critical fields unless the schema explicitly provides an `other` extension mechanism. - During the pilot, prefer additive evolution and publish deprecation windows. - Every proposal and execution outcome stores the schema version used. ## 12. Extensions Extensions are namespaced: ```json { "extensions": { "com.example.carrier.temperatureClass": "chilled" } } ``` An extension cannot change core semantics, authorization or lifecycle behaviour. If multiple participants need the same extension, promote it through protocol governance after conformance examples exist. ## 13. Conformance A conformant implementation must pass tests for: - JSON Schema validation; - lifecycle transitions; - idempotent retries; - stale-write rejection; - bilateral visibility isolation; - field-level disclosure; - duplicate and out-of-order events; - proposal expiry and withdrawal; - explicit decision policy; - execution-outcome deduplication and correction history; - RFC 9457 error responses. Conformance profiles: - **Publisher:** capacity offers and/or load requests; - **Consumer:** discovers and decides; - **Broker:** authorization, matching, lifecycle and events; - **Outcome provider:** submits execution status and references; - **Full reference client:** implements publisher, consumer and evidence flows. ## 14. Adapter strategy Adapters map external formats at the edge: ```text External TMS/EDI/UBL/EPCIS ↓ Adapter layer ↓ PCX contract ↓ Reference broker ``` Rules: - do not expose adapter-specific fields in core schemas without a cross-participant use case; - preserve source and destination external references for traceability; - bind connector processing to the authenticated organization, never to an inbound tenant field; - document lossy mappings; - quarantine invalid, ambiguous or unsafe inbound messages; - include adapter and supported-profile versions in diagnostics; - do not silently invent required values; - process commands and events idempotently with durable inbox, outbox and mapping state; - reconcile authoritative resources after missed delivery or downtime; - expose health, lag, dead letters and mapping failures; - test tenant/organization isolation independently for every adapter; - avoid direct writes to a connected system's internal database unless it documents that interface as supported. ## 15. Items requiring validation before v0.1 - canonical execution handover schema and movement boundaries; - tender counterproposal, amendment and cancellation semantics; - OAuth/OIDC client authentication, claims, rotation and revocation profile; - organization verification and connector registration model; - event signing, replay resistance and reconciliation profile; - exact vehicle/body-type vocabulary; - area representation: circle, polygon, administrative region or combinations; - recurrence model for fixed corridors; - bilateral decision policy and commitment semantics; - retention and withdrawal rules; - webhook signing method; - legal organization identifier strategy; - price inclusion or complete exclusion; - dangerous goods and temperature-control minimum fields; - kilometer baseline calculation specification; - license, trademark and governance owner.