offering-protocol 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. offering_protocol-0.1.0/.gitignore +12 -0
  2. offering_protocol-0.1.0/LICENSE +21 -0
  3. offering_protocol-0.1.0/PKG-INFO +362 -0
  4. offering_protocol-0.1.0/README.md +333 -0
  5. offering_protocol-0.1.0/pyproject.toml +100 -0
  6. offering_protocol-0.1.0/src/offering_protocol/__init__.py +7 -0
  7. offering_protocol-0.1.0/src/offering_protocol/agent/__init__.py +66 -0
  8. offering_protocol-0.1.0/src/offering_protocol/agent/agent.py +135 -0
  9. offering_protocol-0.1.0/src/offering_protocol/agent/cache.py +51 -0
  10. offering_protocol-0.1.0/src/offering_protocol/agent/capabilities.py +262 -0
  11. offering_protocol-0.1.0/src/offering_protocol/agent/client.py +667 -0
  12. offering_protocol-0.1.0/src/offering_protocol/agent/details.py +227 -0
  13. offering_protocol-0.1.0/src/offering_protocol/agent/schema.py +129 -0
  14. offering_protocol-0.1.0/src/offering_protocol/core/__init__.py +64 -0
  15. offering_protocol-0.1.0/src/offering_protocol/core/models.py +475 -0
  16. offering_protocol-0.1.0/src/offering_protocol/core/references.py +135 -0
  17. offering_protocol-0.1.0/src/offering_protocol/core/schemas/action-relation.schema.json +9 -0
  18. offering_protocol-0.1.0/src/offering_protocol/core/schemas/action-request.schema.json +18 -0
  19. offering_protocol-0.1.0/src/offering_protocol/core/schemas/action.schema.json +56 -0
  20. offering_protocol-0.1.0/src/offering_protocol/core/schemas/attribute-schema-reference.schema.json +6 -0
  21. offering_protocol-0.1.0/src/offering_protocol/core/schemas/authentication-requirement.schema.json +11 -0
  22. offering_protocol-0.1.0/src/offering_protocol/core/schemas/capability-identifier.schema.json +9 -0
  23. offering_protocol-0.1.0/src/offering_protocol/core/schemas/capability-link.schema.json +14 -0
  24. offering_protocol-0.1.0/src/offering_protocol/core/schemas/collection-search-request.schema.json +45 -0
  25. offering_protocol-0.1.0/src/offering_protocol/core/schemas/collection.schema.json +72 -0
  26. offering_protocol-0.1.0/src/offering_protocol/core/schemas/detail-fields.schema.json +16 -0
  27. offering_protocol-0.1.0/src/offering_protocol/core/schemas/enrollment-protocol.schema.json +15 -0
  28. offering_protocol-0.1.0/src/offering_protocol/core/schemas/filter-capability-source.schema.json +41 -0
  29. offering_protocol-0.1.0/src/offering_protocol/core/schemas/filter-definition-page.schema.json +25 -0
  30. offering_protocol-0.1.0/src/offering_protocol/core/schemas/filter-definition.schema.json +63 -0
  31. offering_protocol-0.1.0/src/offering_protocol/core/schemas/filter-expression.schema.json +45 -0
  32. offering_protocol-0.1.0/src/offering_protocol/core/schemas/filter-operator.schema.json +14 -0
  33. offering_protocol-0.1.0/src/offering_protocol/core/schemas/filter-type.schema.json +14 -0
  34. offering_protocol-0.1.0/src/offering_protocol/core/schemas/filter-unit.schema.json +45 -0
  35. offering_protocol-0.1.0/src/offering_protocol/core/schemas/http-action-target.schema.json +36 -0
  36. offering_protocol-0.1.0/src/offering_protocol/core/schemas/invalid-parameter.schema.json +55 -0
  37. offering_protocol-0.1.0/src/offering_protocol/core/schemas/local-resource-identifier-list.schema.json +10 -0
  38. offering_protocol-0.1.0/src/offering_protocol/core/schemas/local-resource-identifier.schema.json +10 -0
  39. offering_protocol-0.1.0/src/offering_protocol/core/schemas/mcp-endpoint.schema.json +29 -0
  40. offering_protocol-0.1.0/src/offering_protocol/core/schemas/offering-search-request.schema.json +68 -0
  41. offering_protocol-0.1.0/src/offering_protocol/core/schemas/offering-search-response.schema.json +20 -0
  42. offering_protocol-0.1.0/src/offering_protocol/core/schemas/offering.schema.json +92 -0
  43. offering_protocol-0.1.0/src/offering_protocol/core/schemas/openapi-action-target.schema.json +20 -0
  44. offering_protocol-0.1.0/src/offering_protocol/core/schemas/operation-descriptor.schema.json +27 -0
  45. offering_protocol-0.1.0/src/offering_protocol/core/schemas/page-envelope.schema.json +25 -0
  46. offering_protocol-0.1.0/src/offering_protocol/core/schemas/page-limit.schema.json +8 -0
  47. offering_protocol-0.1.0/src/offering_protocol/core/schemas/payment-option.schema.json +24 -0
  48. offering_protocol-0.1.0/src/offering_protocol/core/schemas/payment-protocol.schema.json +34 -0
  49. offering_protocol-0.1.0/src/offering_protocol/core/schemas/price-preview.schema.json +133 -0
  50. offering_protocol-0.1.0/src/offering_protocol/core/schemas/problem-code.schema.json +9 -0
  51. offering_protocol-0.1.0/src/offering_protocol/core/schemas/problem-details.schema.json +66 -0
  52. offering_protocol-0.1.0/src/offering_protocol/core/schemas/protocol-version.schema.json +8 -0
  53. offering_protocol-0.1.0/src/offering_protocol/core/schemas/refinement-bucket.schema.json +28 -0
  54. offering_protocol-0.1.0/src/offering_protocol/core/schemas/refinement-group.schema.json +24 -0
  55. offering_protocol-0.1.0/src/offering_protocol/core/schemas/representation.schema.json +11 -0
  56. offering_protocol-0.1.0/src/offering_protocol/core/schemas/resource-identity.schema.json +27 -0
  57. offering_protocol-0.1.0/src/offering_protocol/core/schemas/resource-image.schema.json +40 -0
  58. offering_protocol-0.1.0/src/offering_protocol/core/schemas/resource-reference.schema.json +19 -0
  59. offering_protocol-0.1.0/src/offering_protocol/core/schemas/schema-reference.schema.json +15 -0
  60. offering_protocol-0.1.0/src/offering_protocol/core/schemas/search-capabilities.schema.json +26 -0
  61. offering_protocol-0.1.0/src/offering_protocol/core/schemas/service-branding-image.schema.json +23 -0
  62. offering_protocol-0.1.0/src/offering_protocol/core/schemas/service-branding.schema.json +19 -0
  63. offering_protocol-0.1.0/src/offering_protocol/core/schemas/service-document.schema.json +337 -0
  64. offering_protocol-0.1.0/src/offering_protocol/core/schemas/service-openapi.schema.json +15 -0
  65. offering_protocol-0.1.0/src/offering_protocol/core/schemas/service-origin.schema.json +9 -0
  66. offering_protocol-0.1.0/src/offering_protocol/core/schemas/service-protocols.schema.json +101 -0
  67. offering_protocol-0.1.0/src/offering_protocol/core/schemas/sort-capability-source.schema.json +41 -0
  68. offering_protocol-0.1.0/src/offering_protocol/core/schemas/sort-definition-page.schema.json +25 -0
  69. offering_protocol-0.1.0/src/offering_protocol/core/schemas/sort-definition.schema.json +35 -0
  70. offering_protocol-0.1.0/src/offering_protocol/core/schemas/sort-key.schema.json +28 -0
  71. offering_protocol-0.1.0/src/offering_protocol/core/schemas/top-level-document.schema.json +16 -0
  72. offering_protocol-0.1.0/src/offering_protocol/core/schemas/trust-protocol.schema.json +15 -0
  73. offering_protocol-0.1.0/src/offering_protocol/core/validation.py +390 -0
  74. offering_protocol-0.1.0/src/offering_protocol/directory/__init__.py +51 -0
  75. offering_protocol-0.1.0/src/offering_protocol/directory/client.py +206 -0
  76. offering_protocol-0.1.0/src/offering_protocol/directory/models.py +103 -0
  77. offering_protocol-0.1.0/src/offering_protocol/directory/transport.py +145 -0
  78. offering_protocol-0.1.0/src/offering_protocol/py.typed +1 -0
  79. offering_protocol-0.1.0/src/offering_protocol/service/__init__.py +32 -0
  80. offering_protocol-0.1.0/src/offering_protocol/service/service.py +444 -0
  81. offering_protocol-0.1.0/src/offering_protocol/service/static_catalog.py +258 -0
@@ -0,0 +1,12 @@
1
+ /.conformance/
2
+ /.mypy_cache/
3
+ /.pytest_cache/
4
+ /.ruff_cache/
5
+ /.venv/
6
+ /dist/
7
+ /htmlcov/
8
+ /src/*.egg-info/
9
+ /.coverage
10
+ /coverage.xml
11
+ __pycache__/
12
+ *.py[cod]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Offering Discovery Protocol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,362 @@
1
+ Metadata-Version: 2.5
2
+ Name: offering-protocol
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Offering Discovery Protocol
5
+ Project-URL: Documentation, https://www.offeringprotocol.org/
6
+ Project-URL: Issues, https://github.com/offering-protocol/odp-python/issues
7
+ Project-URL: Repository, https://github.com/offering-protocol/odp-python
8
+ Author-email: Nas Kavian <nas@inflowpay.ai>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,discovery,odp,offering,protocol
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: httpx<1,>=0.28.1
24
+ Requires-Dist: jsonschema<5,>=4.26.0
25
+ Requires-Dist: pydantic<3,>=2.13.4
26
+ Requires-Dist: referencing<1,>=0.28.4
27
+ Requires-Dist: rfc3986-validator<1,>0.1.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Offering Discovery Protocol for Python
31
+
32
+ [![CI](https://github.com/offering-protocol/odp-python/actions/workflows/ci.yml/badge.svg)](https://github.com/offering-protocol/odp-python/actions/workflows/ci.yml)
33
+ [![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
34
+ [![PyPI](https://img.shields.io/pypi/v/offering-protocol)](https://pypi.org/project/offering-protocol/)
35
+ [![Codecov](https://codecov.io/gh/offering-protocol/odp-python/graph/badge.svg)](https://codecov.io/gh/offering-protocol/odp-python)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
37
+
38
+ Official Python software development kit for the
39
+ [Offering Discovery Protocol](https://www.offeringprotocol.org/), the open protocol for discovering
40
+ Services and navigating their Offerings.
41
+
42
+ ODP separates two levels of discovery:
43
+
44
+ 1. An Agent searches the canonical Directory for Services.
45
+ 2. The Agent inspects a Service's live ODP document and navigates that Service's Collections and
46
+ Offerings.
47
+
48
+ The Directory does not copy every Service catalog. Catalog searches go directly to each Service.
49
+
50
+ ## Installation
51
+
52
+ ```sh
53
+ python -m pip install offering-protocol
54
+ ```
55
+
56
+ Python 3.11 or newer is required. The distribution provides one typed package with modules for each
57
+ integration role:
58
+
59
+ | Goal | Module |
60
+ | --- | --- |
61
+ | Parse protocol models and validate normative documents | `offering_protocol.core` |
62
+ | Search the canonical production or sandbox Directory | `offering_protocol.directory` |
63
+ | Inspect Services and navigate their catalogs | `offering_protocol.agent` |
64
+ | Publish an ODP Service | `offering_protocol.service` |
65
+
66
+ ## Search the Directory
67
+
68
+ `DirectoryClient` uses the one canonical production Directory. Pass `Environment.SANDBOX` when
69
+ working against InFlow's sandbox; the endpoint itself is not configurable.
70
+
71
+ ```python
72
+ import asyncio
73
+
74
+ from offering_protocol.directory import DirectoryClient, Environment, SearchRequest, ServiceFilters
75
+
76
+
77
+ async def main() -> None:
78
+ async with DirectoryClient(Environment.PRODUCTION) as directory:
79
+ page = await directory.search(
80
+ SearchRequest(
81
+ query="indoor plants",
82
+ filters=ServiceFilters(keywords=["plants"]),
83
+ limit=20,
84
+ )
85
+ )
86
+ for service in page.items:
87
+ print(service.name, service.service_origin)
88
+
89
+ if page.next:
90
+ next_page = await directory.continue_search(page.next)
91
+ print(f"Next page contains {len(next_page.items)} Services")
92
+
93
+
94
+ asyncio.run(main())
95
+ ```
96
+
97
+ Use `search_services()` when the application wants bounded automatic pagination. Use `suggest()` to
98
+ discover keyword completions supported by the Directory.
99
+
100
+ ## Inspect and navigate a Service
101
+
102
+ `ServiceClient` checks the Service document before calling an operation. Calling an operation the
103
+ Service does not advertise raises `UnsupportedOperationError` before a catalog request is sent.
104
+
105
+ ```python
106
+ import asyncio
107
+
108
+ from offering_protocol.agent import ServiceClient
109
+ from offering_protocol.core import OfferingSearchRequest, Representation
110
+
111
+
112
+ async def main() -> None:
113
+ async with ServiceClient("https://demo.inflowpay.ai") as service:
114
+ inspection = await service.inspect()
115
+ print(inspection.document.name)
116
+ print([operation.name.value for operation in inspection.document.operations])
117
+ protocols = inspection.document.protocols
118
+ print([protocol.name.value for protocol in protocols.trust] if protocols else [])
119
+
120
+ page = await service.search_offerings(
121
+ OfferingSearchRequest(query="plant"),
122
+ Representation.TERSE,
123
+ )
124
+ for offering in page.items:
125
+ print(offering.id, offering.name, offering.price)
126
+
127
+ if page.items:
128
+ details = await service.get_offering_details(page.items[0].id)
129
+ for action in details.actions:
130
+ print(action.id, action.rel.value, action.authentication.value)
131
+
132
+
133
+ asyncio.run(main())
134
+ ```
135
+
136
+ `get_offering_details()` resolves and validates an Offering's Attribute Schema, normalizes usable
137
+ Actions, and reports non-fatal issues separately from the Offering. `resolve_action()` resolves a
138
+ specific Action's HTTP or OpenAPI target and request schema. It never calls the target, enrolls,
139
+ authenticates, or pays.
140
+
141
+ The Agent module also provides:
142
+
143
+ - Collection list, get, search, and bounded traversal operations.
144
+ - Offering list, get, search, collection listing, continuation, and bounded traversal operations.
145
+ - Effective inline and linked Filter and Sort definitions for Service and Collection scopes.
146
+ - Directory-to-Service federated Offering discovery through `Agent`.
147
+ - Conditional request and representation caching with injectable `Cache` and `Transport` protocols.
148
+
149
+ Default fallback cache lifetimes are four hours for Service documents, one hour for Collections,
150
+ and five minutes for Offerings. HTTP cache directives take precedence. Provide distinct `transport`
151
+ and `supporting_transport` instances when protocol resources and linked schemas require different
152
+ credentials or network policy.
153
+
154
+ ### Search across Services
155
+
156
+ `Agent` composes Directory search with bounded concurrent searches of the returned Services. A
157
+ failure from one Service becomes an issue event instead of terminating results from the other
158
+ Services.
159
+
160
+ ```python
161
+ from offering_protocol.agent import Agent, FederatedSearchRequest
162
+ from offering_protocol.core import OfferingSearchRequest
163
+ from offering_protocol.directory import SearchRequest
164
+
165
+
166
+ async with Agent() as agent:
167
+ events = await agent.search_offerings_across_services(
168
+ FederatedSearchRequest(
169
+ services=SearchRequest(query="plant stores"),
170
+ offerings=OfferingSearchRequest(query="rubber plant"),
171
+ max_services=20,
172
+ max_offerings_per_service=10,
173
+ )
174
+ )
175
+ for event in events:
176
+ if event.offering is not None:
177
+ print(event.service.name, event.offering.name)
178
+ else:
179
+ print(event.service.name, event.issue)
180
+ ```
181
+
182
+ ### Search capabilities and Actions
183
+
184
+ Search capability resolution combines inline and linked Filter and Sort definitions into the
185
+ effective definitions available at a Service or Collection scope:
186
+
187
+ ```python
188
+ capabilities = await service.get_offering_search_capabilities()
189
+ for identifier, definition in capabilities.filters.items():
190
+ print(identifier, definition.operators)
191
+ for issue in capabilities.issues:
192
+ print(issue.message)
193
+ ```
194
+
195
+ After selecting an Offering, resolve an advertised Action by its identifier:
196
+
197
+ ```python
198
+ resolved = await service.resolve_action("rubber-plant", "purchase")
199
+ if resolved.action.http is not None:
200
+ print(resolved.action.http.url)
201
+ elif resolved.action.openapi is not None:
202
+ print(resolved.action.openapi.url)
203
+ print(resolved.request_schema)
204
+ ```
205
+
206
+ Resolution returns metadata only. The application decides whether to enroll, authenticate, pay, or
207
+ invoke the resolved target.
208
+
209
+ ### Caching and HTTP transport
210
+
211
+ `MemoryCache` is the default process-local cache. Implement the `Cache` protocol when representations
212
+ must survive process restarts or share storage across workers. A custom `Transport` implements
213
+ asynchronous `send()` and `aclose()` methods. Caller-provided caches and transports remain owned by
214
+ the caller.
215
+
216
+ The built-in HTTP transport resolves and validates every destination before connecting, pins the
217
+ connection to a validated public address, does not inherit proxy settings from the environment, and
218
+ sends supporting-document requests without credentials. A custom transport must preserve those ODP
219
+ network and credential-isolation requirements. Local HTTP development is disabled by default; pass
220
+ `allow_local_network=True` to `ServiceClient` only for an explicit `localhost`, `127.0.0.1`, or
221
+ `[::1]` development Service.
222
+
223
+ Attribute Schema resolution accepts JSON Schema Draft 2020-12, loads at most 16 documents through
224
+ eight reference levels, and limits the complete schema graph to one mebibyte. Linked schema
225
+ documents must use HTTPS. Cross-document schema composition uses `$ref`; `$dynamicRef` accepts only
226
+ a fragment reference such as `#node`.
227
+
228
+ ## Publish a Service
229
+
230
+ `Service` is framework-neutral. Adapt the incoming framework request to `Request`, call
231
+ `Service.handle()`, and copy the returned status, headers, and body into the framework response.
232
+
233
+ ```python
234
+ from offering_protocol.core import Collection, Offering, Protocol, TrustProtocol
235
+ from offering_protocol.service import ServiceBuilder, StaticCatalog, StaticCatalogOptions
236
+
237
+ catalog = StaticCatalog(
238
+ StaticCatalogOptions(
239
+ collections=(Collection(id="plants", name="Plants", odp_version="1.0"),),
240
+ offerings=(
241
+ Offering(
242
+ collection_ids=["plants"],
243
+ description="A resilient indoor plant.",
244
+ id="rubber-plant",
245
+ name="Rubber Plant",
246
+ odp_version="1.0",
247
+ ),
248
+ ),
249
+ )
250
+ )
251
+
252
+ service = (
253
+ ServiceBuilder(
254
+ name="Indica Flowers",
255
+ description="An AI-enabled store for houseplants and plant care.",
256
+ language="en",
257
+ endpoint_base="/odp",
258
+ )
259
+ .keywords(["houseplants", "indoor-plants"])
260
+ .protocols([], [], [TrustProtocol(name=Protocol.TAP)])
261
+ .website_url("https://example.com")
262
+ .build(catalog)
263
+ )
264
+ ```
265
+
266
+ Every Service integration must implement `list-offerings` and `get-offering`. `StaticCatalog` is the
267
+ small-Service implementation: it adds Collection operations when Collections are provided and uses
268
+ integrity-protected, stateless continuations that expire after one hour. Larger Services can
269
+ implement the typed `Catalog` protocol over their existing indexed catalog and search infrastructure.
270
+
271
+ Service responses are validated against the bundled normative schemas before they are returned.
272
+ The handler enforces fixed operation paths and methods, ODP media types, request and response byte
273
+ limits, local identifiers, page limits, and protocol Problem Details.
274
+
275
+ See [examples/README.md](./examples/README.md) for a runnable Service and Agent.
276
+
277
+ ## Protocol composition
278
+
279
+ ODP discovers what a Service offers and how an Agent can act on an Offering. A Service document and
280
+ its Actions can advertise enrollment, payment, and trust protocols, but ODP does not create
281
+ credentials, invoke Actions, submit payments, or implement trust protocols. Applications compose
282
+ the appropriate protocol clients around an Action resolved through ODP.
283
+
284
+ ## Errors and validation
285
+
286
+ Each role exposes typed errors:
287
+
288
+ - `OdpValidationError` includes deterministic schema and semantic issues.
289
+ - `DirectoryError` and `DirectoryRequestError` describe canonical Directory failures.
290
+ - `AgentError`, `ServiceRequestError`, and `UnsupportedOperationError` describe Agent-side failures.
291
+ - `ServiceError`, `CatalogError`, and `RequestError` describe Service integration failures.
292
+
293
+ Protocol models preserve additive members in `model.additional` and round-trip them through
294
+ `model.to_dict()`. Parsing remains strict for normative constraints and fields that prohibit unknown
295
+ members.
296
+
297
+ Handle the narrowest error that the application can act upon and use the role's base error for the
298
+ remaining failures:
299
+
300
+ ```python
301
+ from offering_protocol.agent import AgentError, ServiceRequestError, UnsupportedOperationError
302
+
303
+ try:
304
+ offering = await service.get_offering("rubber-plant")
305
+ except UnsupportedOperationError as error:
306
+ print(f"Service does not advertise {error.operation.value}")
307
+ except ServiceRequestError as error:
308
+ print(error.status, error.headers)
309
+ except AgentError as error:
310
+ print(error)
311
+ ```
312
+
313
+ ## Development
314
+
315
+ Python 3.11 or newer and [uv](https://docs.astral.sh/uv/) are required.
316
+
317
+ ```sh
318
+ make sync
319
+ make verify
320
+ ```
321
+
322
+ Format source files with:
323
+
324
+ ```sh
325
+ make format
326
+ ```
327
+
328
+ The merge gate checks formatting, linting, strict type checking, 100 percent line and branch
329
+ coverage, distribution metadata, bundled runtime schemas, and installation of the built wheel into
330
+ a clean virtual environment.
331
+
332
+ Generate Agent and Service conformance reports with:
333
+
334
+ ```sh
335
+ ODP_SPECS_DIR=/path/to/odp-specs make conformance
336
+ ```
337
+
338
+ The language-neutral harness executes the package's public behavior and writes release evidence to
339
+ `.conformance/reports/`.
340
+
341
+ Run the Python Agent against the Node.js reference Service with:
342
+
343
+ ```sh
344
+ ODP_NODE_DIR=/path/to/odp-node make interoperability
345
+ ```
346
+
347
+ See [`odp-specs`](https://github.com/offering-protocol/odp-specs) for the normative draft, schemas,
348
+ examples, and test vectors.
349
+
350
+ ## Security
351
+
352
+ See [SECURITY.md](./SECURITY.md) for vulnerability reporting.
353
+
354
+ ## Releases
355
+
356
+ Maintainers run the `Release` workflow from `main`. It verifies the package and a clean consumer,
357
+ publishes through PyPI Trusted Publishing, attests the distributions, and creates the matching tag
358
+ and GitHub release with Agent and Service conformance reports.
359
+
360
+ ## License
361
+
362
+ MIT.