layr8 0.2.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 (60) hide show
  1. layr8-0.2.0/.claude/CLAUDE.md +37 -0
  2. layr8-0.2.0/.claude/skills/build-layr8-agent.md +371 -0
  3. layr8-0.2.0/.github/workflows/ci.yaml +42 -0
  4. layr8-0.2.0/.github/workflows/release.yaml +120 -0
  5. layr8-0.2.0/.gitignore +7 -0
  6. layr8-0.2.0/CONTEXT.md +17 -0
  7. layr8-0.2.0/PKG-INFO +14 -0
  8. layr8-0.2.0/README.md +537 -0
  9. layr8-0.2.0/compat/Dockerfile +12 -0
  10. layr8-0.2.0/compat/bin/__init__.py +1 -0
  11. layr8-0.2.0/compat/bin/compat.py +104 -0
  12. layr8-0.2.0/compat/cloud_nodes.json +7 -0
  13. layr8-0.2.0/compat/conftest.py +6 -0
  14. layr8-0.2.0/compat/pyproject.toml +27 -0
  15. layr8-0.2.0/compat/scenarios/__init__.py +0 -0
  16. layr8-0.2.0/compat/scenarios/disconnected.py +56 -0
  17. layr8-0.2.0/compat/scenarios/echo.py +68 -0
  18. layr8-0.2.0/compat/scenarios/pass_scenario.py +66 -0
  19. layr8-0.2.0/compat/scenarios/types.py +39 -0
  20. layr8-0.2.0/compat/scenarios/wildcard.py +71 -0
  21. layr8-0.2.0/compat/tests/conftest.py +88 -0
  22. layr8-0.2.0/compat/tests/test_cli.py +37 -0
  23. layr8-0.2.0/compat/tests/test_disconnected.py +33 -0
  24. layr8-0.2.0/compat/tests/test_echo.py +186 -0
  25. layr8-0.2.0/compat/tests/test_pass.py +51 -0
  26. layr8-0.2.0/compat/tests/test_types.py +72 -0
  27. layr8-0.2.0/compat/tests/test_wildcard.py +50 -0
  28. layr8-0.2.0/docs/RELEASE-SETUP.md +72 -0
  29. layr8-0.2.0/docs/superpowers/plans/2026-05-15-reply-protocol.md +1241 -0
  30. layr8-0.2.0/docs/superpowers/plans/2026-05-17-compat-cli-fixes.md +1012 -0
  31. layr8-0.2.0/docs/superpowers/specs/2026-05-15-reply-protocol-design.md +112 -0
  32. layr8-0.2.0/docs/superpowers/specs/2026-05-17-compat-cli-fixes-release-pipeline.md +180 -0
  33. layr8-0.2.0/examples/chat.py +88 -0
  34. layr8-0.2.0/examples/durable_handler.py +66 -0
  35. layr8-0.2.0/examples/echo_agent.py +194 -0
  36. layr8-0.2.0/notes/features/prd-python-sdk-compat.md +251 -0
  37. layr8-0.2.0/pyproject.toml +28 -0
  38. layr8-0.2.0/src/layr8/__init__.py +46 -0
  39. layr8-0.2.0/src/layr8/backoff.py +22 -0
  40. layr8-0.2.0/src/layr8/channel.py +454 -0
  41. layr8-0.2.0/src/layr8/client.py +685 -0
  42. layr8-0.2.0/src/layr8/config.py +59 -0
  43. layr8-0.2.0/src/layr8/credentials.py +58 -0
  44. layr8-0.2.0/src/layr8/errors.py +130 -0
  45. layr8-0.2.0/src/layr8/handler.py +71 -0
  46. layr8-0.2.0/src/layr8/message.py +196 -0
  47. layr8-0.2.0/src/layr8/presentations.py +14 -0
  48. layr8-0.2.0/src/layr8/rest.py +146 -0
  49. layr8-0.2.0/src/layr8/sentinel.py +23 -0
  50. layr8-0.2.0/tests/__init__.py +0 -0
  51. layr8-0.2.0/tests/integration_test.py +361 -0
  52. layr8-0.2.0/tests/test_client.py +988 -0
  53. layr8-0.2.0/tests/test_config.py +58 -0
  54. layr8-0.2.0/tests/test_credentials.py +331 -0
  55. layr8-0.2.0/tests/test_handler.py +97 -0
  56. layr8-0.2.0/tests/test_message.py +244 -0
  57. layr8-0.2.0/tests/test_presentations.py +168 -0
  58. layr8-0.2.0/tests/test_reconnect.py +158 -0
  59. layr8-0.2.0/tests/test_rest.py +159 -0
  60. layr8-0.2.0/tests/test_sentinel.py +28 -0
@@ -0,0 +1,37 @@
1
+ # Python SDK Principles
2
+
3
+ ## Public Repository
4
+
5
+ This is a public repository. Every commit is visible to the world.
6
+
7
+ ### Before every commit, verify:
8
+
9
+ - **No real API keys, passwords, or tokens.** Test keys must be obviously fake (e.g., contain `testkey` in the string).
10
+ - **No internal infrastructure references.** No cloud account IDs, cluster names, or internal domain names. Only `*.localhost` and `example.com` are acceptable.
11
+ - **No internal documentation links.** No references to private repos, internal wikis, or private channels.
12
+ - **No customer data or PII.**
13
+
14
+ ### Acceptable
15
+
16
+ - Local-dev test keys with obvious patterns (e.g., `alice_abcd1234_testkeyalicetestkeyali24`)
17
+ - `*.localhost` URLs for local development
18
+ - `did:web:*.localhost:*` test DIDs
19
+ - Unit test sentinel values like `"test-api-key"`
20
+
21
+ ### Not acceptable
22
+
23
+ - Keys that follow production format without obvious test markers
24
+ - Internal service URLs (`.internal`, `.corp`, `.svc.cluster.local`)
25
+ - `.env` files with real values (`.env.example` with placeholders is fine)
26
+
27
+ ## Testing
28
+
29
+ - Run tests before every commit
30
+ - Integration tests require a local dev environment with two cloud-nodes (alice-test, bob-test)
31
+
32
+ ## Conventions
33
+
34
+ - Async/await with `asyncio`
35
+ - `@dataclass` for structured types (`Credential`, `StoredCredential`, `VerifiedPresentation`)
36
+ - Custom `aiohttp` resolver (`_LocalhostResolver`) for `*.localhost` resolution (RFC 6761)
37
+ - Snake_case for all public API methods (e.g., `sign_credential`, `verify_presentation`)
@@ -0,0 +1,371 @@
1
+ ---
2
+ name: build-layr8-agent
3
+ description: Use when building a Python agent for the Layr8 platform. Covers the full SDK API — config, handlers, messaging, error handling, and DIDComm conventions.
4
+ ---
5
+
6
+ # Building Layr8 Agents with the Python SDK
7
+
8
+ Full documentation: https://docs.layr8.io/reference/python-sdk
9
+
10
+ ## Import
11
+
12
+ ```python
13
+ from layr8 import Client, Config, Message, log_errors
14
+ ```
15
+
16
+ Requires Python 3.11+. The SDK is fully async, built on `asyncio` and `websockets`.
17
+
18
+ ## Config
19
+
20
+ ```python
21
+ client = Client(Config(
22
+ node_url="ws://mynode.localhost/plugin_socket/websocket",
23
+ api_key="my_api_key",
24
+ agent_did="did:web:mynode.localhost:my-agent",
25
+ ), log_errors())
26
+ ```
27
+
28
+ The `Client` constructor takes two required arguments:
29
+ 1. `Config(...)` — connection configuration
30
+ 2. `on_error` — a `Callable[[SDKError], None]` that receives all SDK-level errors
31
+
32
+ Use `log_errors()` for a convenient default that logs errors via `logging.getLogger("layr8")`.
33
+
34
+ All Config fields fall back to environment variables if empty:
35
+ - `node_url` → `LAYR8_NODE_URL`
36
+ - `api_key` → `LAYR8_API_KEY`
37
+ - `agent_did` → `LAYR8_AGENT_DID`
38
+
39
+ `agent_did` is optional — if omitted, the node assigns an ephemeral DID on connect.
40
+
41
+ ## Lifecycle
42
+
43
+ ```
44
+ Client(Config, on_error) → handle (register handlers) → connect → ... → close
45
+ ```
46
+
47
+ Or using the async context manager:
48
+
49
+ ```
50
+ Client(Config, on_error) → handle → async with client: ...
51
+ ```
52
+
53
+ - `handle` must be called BEFORE `connect` — raises `AlreadyConnectedError` after.
54
+ - `connect()` establishes WebSocket and joins the Phoenix Channel. Returns a coroutine.
55
+ - `close()` sends `phx_leave` and shuts down gracefully. Returns a coroutine.
56
+ - `did` (property) returns the agent's DID (explicit or node-assigned).
57
+ - Supports `async with` context manager (`__aenter__`/`__aexit__`).
58
+
59
+ ## Registering Handlers
60
+
61
+ ### Decorator Syntax
62
+
63
+ ```python
64
+ @client.handle("https://layr8.io/protocols/echo/1.0/request")
65
+ async def echo(msg: Message) -> Message:
66
+ body = msg.unmarshal_body()
67
+ return Message(
68
+ type="https://layr8.io/protocols/echo/1.0/response",
69
+ body={"echo": body["message"]},
70
+ )
71
+ ```
72
+
73
+ ### Direct Call
74
+
75
+ ```python
76
+ async def echo(msg: Message) -> Message:
77
+ body = msg.unmarshal_body()
78
+ return Message(
79
+ type="https://layr8.io/protocols/echo/1.0/response",
80
+ body={"echo": body["message"]},
81
+ )
82
+
83
+ client.handle("https://layr8.io/protocols/echo/1.0/request", echo)
84
+ ```
85
+
86
+ Handler return values:
87
+ - `Message(...)` → send response to sender (auto-fills `id`, `from_`, `to`, `thread_id`)
88
+ - `None` → no response (fire-and-forget inbound)
89
+ - Raised exception → send DIDComm problem report to sender
90
+
91
+ The protocol base URI is derived automatically from the message type
92
+ (last path segment removed) and registered with the node on connect.
93
+
94
+ ## Sending Messages
95
+
96
+ ### Send (with server ack)
97
+
98
+ By default, `send()` waits for the server to acknowledge receipt. If the server rejects the message, a `RuntimeError` is raised.
99
+
100
+ ```python
101
+ await client.send(Message(
102
+ type="https://didcomm.org/basicmessage/2.0/message",
103
+ to=["did:web:other-node:agent"],
104
+ body={"content": "Hello!"},
105
+ ))
106
+ ```
107
+
108
+ ### Fire-and-forget
109
+
110
+ To skip waiting for the server acknowledgment, pass `fire_and_forget=True`:
111
+
112
+ ```python
113
+ await client.send(
114
+ Message(
115
+ type="https://didcomm.org/basicmessage/2.0/message",
116
+ to=["did:web:other-node:agent"],
117
+ body={"content": "Hello!"},
118
+ ),
119
+ fire_and_forget=True,
120
+ )
121
+ ```
122
+
123
+ ### Request/Response
124
+
125
+ ```python
126
+ resp = await client.request(
127
+ Message(
128
+ type="https://layr8.io/protocols/echo/1.0/request",
129
+ to=["did:web:other-node:agent"],
130
+ body={"message": "ping"},
131
+ ),
132
+ timeout=5.0,
133
+ )
134
+
135
+ body = resp.unmarshal_body()
136
+ # resp is the correlated response (matched by thread ID)
137
+ ```
138
+
139
+ ## Message Structure
140
+
141
+ ```python
142
+ @dataclass
143
+ class Message:
144
+ id: str = "" # auto-generated if empty
145
+ type: str = "" # DIDComm message type URI
146
+ from_: str = "" # auto-filled with agent DID (wire: "from")
147
+ to: list[str] = field(default_factory=list) # recipient DIDs
148
+ thread_id: str = "" # auto-generated for request (wire: "thid")
149
+ parent_thread_id: str = "" # set via parent_thread param (wire: "pthid")
150
+ body: Any = None # serialized to JSON
151
+ context: MessageContext | None = None # populated on inbound messages
152
+ ```
153
+
154
+ **Important:** The `from` field is named `from_` because `from` is a Python reserved word. On the wire, it serializes as `"from"`.
155
+
156
+ ### Inbound Message Context
157
+
158
+ ```python
159
+ if msg.context:
160
+ msg.context.authorized # bool — node authorization result
161
+ msg.context.recipient # str — recipient DID
162
+ msg.context.sender_credentials # list[Credential] — each has .id, .name
163
+ ```
164
+
165
+ ### Unmarshaling Body
166
+
167
+ ```python
168
+ # As a dict
169
+ body = msg.unmarshal_body()
170
+
171
+ # As a typed dataclass
172
+ @dataclass
173
+ class EchoRequest:
174
+ message: str
175
+
176
+ body = msg.unmarshal_body(EchoRequest)
177
+ print(body.message) # typed attribute access
178
+ ```
179
+
180
+ ## Options
181
+
182
+ ### Manual Ack
183
+
184
+ By default, messages are auto-acked before the handler runs.
185
+ Use `manual_ack` to control ack timing (e.g., ack only after DB write):
186
+
187
+ ```python
188
+ @client.handle(msg_type, manual_ack=True)
189
+ async def handler(msg: Message) -> Message:
190
+ result = process(msg)
191
+ msg.ack() # explicitly ack after processing
192
+ return Message(type=result_type, body=result)
193
+ ```
194
+
195
+ ### Parent Thread
196
+
197
+ For nested thread correlation:
198
+
199
+ ```python
200
+ resp = await client.request(msg, parent_thread="parent-thread-id", timeout=10.0)
201
+ ```
202
+
203
+ ## Error Handling
204
+
205
+ ### ErrorHandler (on_error callback)
206
+
207
+ The `on_error` callback receives `SDKError` instances for all SDK-level errors:
208
+
209
+ ```python
210
+ from layr8 import SDKError, ErrorKind
211
+
212
+ def my_handler(err: SDKError) -> None:
213
+ print(f"[{err.kind.value}] {err.cause}")
214
+
215
+ client = Client(Config(...), my_handler)
216
+ ```
217
+
218
+ `ErrorKind` values:
219
+ - `PARSE_FAILURE` — inbound message could not be parsed
220
+ - `NO_HANDLER` — no handler registered for the message type
221
+ - `HANDLER_EXCEPTION` — a handler raised an exception
222
+ - `SERVER_REJECT` — the server rejected a sent message
223
+ - `TRANSPORT_WRITE` — failed to write to WebSocket
224
+
225
+ ### Problem Reports
226
+
227
+ When a remote handler raises, `request` raises `ProblemReportError`:
228
+
229
+ ```python
230
+ from layr8 import ProblemReportError
231
+
232
+ try:
233
+ resp = await client.request(msg)
234
+ except ProblemReportError as e:
235
+ print(f"remote error [{e.code}]: {e.comment}")
236
+ ```
237
+
238
+ ### Error Classes
239
+
240
+ - `NotConnectedError` — `send`/`request` called before `connect`
241
+ - `AlreadyConnectedError` — `handle` called after `connect`
242
+ - `ClientClosedError` — `connect` called after `close`
243
+ - `Layr8ConnectionError` — failed to connect to node (`.url`, `.reason`)
244
+
245
+ ```python
246
+ from layr8.errors import Layr8ConnectionError
247
+
248
+ try:
249
+ await client.connect()
250
+ except Layr8ConnectionError as e:
251
+ print(f"failed to connect to {e.url}: {e.reason}")
252
+ ```
253
+
254
+ Note: `request()` raises `asyncio.TimeoutError` on timeout (uses `asyncio.wait_for`).
255
+
256
+ ## Connection Resilience
257
+
258
+ The SDK automatically reconnects when the WebSocket connection drops (node restart, network interruption). Reconnection uses exponential backoff (1s → 2s → 4s → ... → 30s max).
259
+
260
+ During reconnection:
261
+ - `send()`, `request()`, and other operations raise `NotConnectedError` immediately — no message queuing
262
+ - `close()` stops the reconnect loop
263
+
264
+ ```python
265
+ @client.on_disconnect
266
+ def handle_disconnect(err: Exception):
267
+ print(f"connection lost: {err}")
268
+
269
+ @client.on_reconnect
270
+ def handle_reconnect():
271
+ print("reconnected")
272
+ ```
273
+
274
+ `on_disconnect` fires only on unexpected drops, not on `close()`.
275
+
276
+ ## DID and Protocol Conventions
277
+
278
+ ### DID Format
279
+
280
+ ```
281
+ did:web:{node-domain}:{agent-path}
282
+ ```
283
+
284
+ Examples:
285
+ - `did:web:alice-test.localhost:my-agent`
286
+ - `did:web:earth.node.layr8.org:echo-service`
287
+
288
+ ### Protocol URI Format
289
+
290
+ ```
291
+ https://layr8.io/protocols/{name}/{version}/{message-type}
292
+ ```
293
+
294
+ The base URI (without the last segment) is the protocol identifier.
295
+ Example: `https://layr8.io/protocols/echo/1.0/request` → protocol `https://layr8.io/protocols/echo/1.0`
296
+
297
+ ### Standard Protocols
298
+
299
+ - Basic message: `https://didcomm.org/basicmessage/2.0/message`
300
+ - Problem report: `https://didcomm.org/report-problem/2.0/problem-report`
301
+
302
+ ## Complete Example: Echo Agent
303
+
304
+ ```python
305
+ import asyncio
306
+ from layr8 import Client, Config, Message, log_errors
307
+
308
+ ECHO_REQUEST = "https://layr8.io/protocols/echo/1.0/request"
309
+ ECHO_RESPONSE = "https://layr8.io/protocols/echo/1.0/response"
310
+
311
+ client = Client(Config(), log_errors())
312
+
313
+ @client.handle(ECHO_REQUEST)
314
+ async def echo(msg: Message) -> Message:
315
+ body = msg.unmarshal_body()
316
+ return Message(
317
+ type=ECHO_RESPONSE,
318
+ body={"echo": body["message"]},
319
+ )
320
+
321
+ async def main():
322
+ async with client:
323
+ print(f"echo agent running as {client.did}")
324
+ await asyncio.Event().wait()
325
+
326
+ asyncio.run(main())
327
+ ```
328
+
329
+ ## Complete Example: Request/Response Client
330
+
331
+ ```python
332
+ import asyncio
333
+ from layr8 import Client, Config, Message, ProblemReportError, log_errors
334
+
335
+ ECHO_REQUEST = "https://layr8.io/protocols/echo/1.0/request"
336
+
337
+ client = Client(Config(), log_errors())
338
+
339
+ # Must register the protocol even if not handling inbound
340
+ @client.handle(ECHO_REQUEST)
341
+ async def noop(msg: Message) -> None:
342
+ return None
343
+
344
+ async def main():
345
+ async with client:
346
+ try:
347
+ resp = await client.request(
348
+ Message(
349
+ type=ECHO_REQUEST,
350
+ to=["did:web:other-node:echo-agent"],
351
+ body={"message": "Hello!"},
352
+ ),
353
+ timeout=5.0,
354
+ )
355
+
356
+ body = resp.unmarshal_body()
357
+ print(f"response: {body['echo']}")
358
+ except ProblemReportError as e:
359
+ print(f"remote error [{e.code}]: {e.comment}")
360
+ except asyncio.TimeoutError:
361
+ print("request timed out")
362
+
363
+ asyncio.run(main())
364
+ ```
365
+
366
+ ## More Examples
367
+
368
+ See the `examples/` directory in the SDK repo for complete working agents:
369
+ - `examples/echo_agent.py` — minimal echo service
370
+ - `examples/chat.py` — interactive chat client
371
+ - `examples/durable_handler.py` — persist-then-ack with JSON-lines
@@ -0,0 +1,42 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v5
14
+
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v6
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - name: Install dependencies
21
+ run: pip install -e ".[dev]"
22
+
23
+ - name: Test
24
+ run: pytest -v
25
+
26
+ compat-unit:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v5
30
+
31
+ - name: Set up Python
32
+ uses: actions/setup-python@v6
33
+ with:
34
+ python-version: "3.12"
35
+
36
+ - name: Install SDK and compat deps
37
+ run: |
38
+ pip install -e .
39
+ pip install -e "compat/[test]"
40
+
41
+ - name: Compat unit tests
42
+ run: cd compat && pytest tests/ -v --ignore=tests/conftest.py -k "not layer1"
@@ -0,0 +1,120 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v5
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v6
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install dependencies
22
+ run: pip install -e ".[dev]"
23
+
24
+ - name: Test
25
+ run: pytest -v
26
+
27
+ compat-unit:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v5
31
+
32
+ - name: Set up Python
33
+ uses: actions/setup-python@v6
34
+ with:
35
+ python-version: "3.12"
36
+
37
+ - name: Install SDK and compat deps
38
+ run: |
39
+ pip install -e .
40
+ pip install -e "compat/[test]"
41
+
42
+ - name: Compat unit tests
43
+ run: cd compat && pytest tests/ -v --ignore=tests/conftest.py -k "not layer1"
44
+
45
+ validate-version:
46
+ runs-on: ubuntu-latest
47
+ outputs:
48
+ version: ${{ steps.check.outputs.version }}
49
+ steps:
50
+ - uses: actions/checkout@v5
51
+
52
+ - name: Set up Python
53
+ uses: actions/setup-python@v6
54
+ with:
55
+ python-version: "3.12"
56
+
57
+ - name: Validate tag matches pyproject.toml
58
+ id: check
59
+ run: |
60
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
61
+ TOML_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
62
+ if [ "$TAG_VERSION" != "$TOML_VERSION" ]; then
63
+ echo "::error::Tag $TAG_VERSION != pyproject.toml $TOML_VERSION"
64
+ exit 1
65
+ fi
66
+ echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
67
+
68
+ publish-pypi:
69
+ needs: [test, compat-unit, validate-version]
70
+ runs-on: ubuntu-latest
71
+ permissions:
72
+ contents: read
73
+ id-token: write
74
+ environment: pypi
75
+ steps:
76
+ - uses: actions/checkout@v5
77
+
78
+ - name: Set up Python
79
+ uses: actions/setup-python@v6
80
+ with:
81
+ python-version: "3.12"
82
+
83
+ - name: Build
84
+ run: pip install build && python -m build
85
+
86
+ - name: Publish to PyPI
87
+ uses: pypa/gh-action-pypi-publish@release/v1
88
+
89
+ publish-compat-image:
90
+ needs: [publish-pypi, validate-version]
91
+ runs-on: ubuntu-latest
92
+ permissions:
93
+ contents: read
94
+ packages: write
95
+ steps:
96
+ - uses: actions/checkout@v5
97
+
98
+ - name: Log in to ghcr.io
99
+ uses: docker/login-action@v4
100
+ with:
101
+ registry: ghcr.io
102
+ username: ${{ github.actor }}
103
+ password: ${{ secrets.GITHUB_TOKEN }}
104
+
105
+ - name: Build and push compat image
106
+ run: |
107
+ VERSION=${{ needs.validate-version.outputs.version }}
108
+ IMAGE=ghcr.io/layr8/python-sdk/compat
109
+ docker build \
110
+ --build-arg SDK_VERSION=$VERSION \
111
+ -t $IMAGE:$VERSION \
112
+ -f compat/Dockerfile compat/
113
+ docker push $IMAGE:$VERSION
114
+
115
+ # compat-gate:
116
+ # needs: [publish-compat-image, validate-version]
117
+ # uses: layr8/compat-suite/.github/workflows/gate.yml@main
118
+ # with:
119
+ # sdk: python
120
+ # version: ${{ needs.validate-version.outputs.version }}
layr8-0.2.0/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
layr8-0.2.0/CONTEXT.md ADDED
@@ -0,0 +1,17 @@
1
+ # Context — Layr8 Python SDK
2
+
3
+ ## Ubiquitous Language
4
+
5
+ | Term | Definition |
6
+ |---|---|
7
+ | **Agent** | A software process that connects to a cloud-node and exchanges DIDComm v2 messages. An agent is identified by a DID. |
8
+ | **Cloud-node** | A Layr8 infrastructure component that routes DIDComm messages between agents. Agents connect via WebSocket using the Phoenix Channel V2 protocol. |
9
+ | **DID** | Decentralized Identifier — a globally unique agent identity (e.g., `did:web:myorg:my-agent`). May be configured explicitly or assigned by the cloud-node on connect. |
10
+ | **Handler** | An async function registered for a specific DIDComm message type. Receives a `Message`, returns a response `Message`, `None`, or `PASS`. |
11
+ | **PASS** | A sentinel value returned by a handler to decline a message — signals to the cloud-node that this agent does not handle this message type. |
12
+ | **Scenario** | A compat-suite test case. Each scenario is a pair of async functions (`run_receiver`, `run_sender`) that exercise a specific SDK behavior against a cloud-node. |
13
+ | **Compat image** | A Docker image (`ghcr.io/layr8/python-sdk/compat:{version}`) that packages the scenario code and CLI adapter. Consumed by the compat-suite orchestrator. |
14
+ | **Ready signal** | A JSON line (`{"status":"ready","did":"..."}`) printed to stdout by a receiver process after connecting and registering handlers. The compat-suite orchestrator waits for this before launching the sender. |
15
+ | **Layer 1** | Pytest + testcontainers adapter — runs scenarios against real cloud-node Docker containers. |
16
+ | **Layer 2** | CLI adapter — implements the compat-suite orchestrator's interface (`--mode`, `--scenario`, `--node`, `--did`). |
17
+ | **Compat-suite orchestrator** | A separate repo (`layr8/compat-suite`) that pairs SDK compat images across languages and cloud-node versions, runs test matrices, and produces compatibility reports. |
layr8-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: layr8
3
+ Version: 0.2.0
4
+ Summary: Layr8 DIDComm Agent SDK for Python
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: aiohttp>=3.9
8
+ Requires-Dist: websockets>=13.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
12
+ Description-Content-Type: text/plain
13
+
14
+ Layr8 DIDComm Agent SDK for Python