lightfall-utils 0.1.0__tar.gz → 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.
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/PKG-INFO +7 -1
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/README.md +1 -0
- lightfall_utils-0.2.0/docs/ipc.md +206 -0
- lightfall_utils-0.2.0/docs/superpowers/plans/2026-09-10-ipc-extraction.md +429 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/pyproject.toml +4 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/_version.py +2 -2
- lightfall_utils-0.2.0/src/lightfall_utils/ipc/__init__.py +45 -0
- lightfall_utils-0.2.0/src/lightfall_utils/ipc/local_server.py +172 -0
- lightfall_utils-0.2.0/src/lightfall_utils/ipc/protocol.py +59 -0
- lightfall_utils-0.2.0/src/lightfall_utils/ipc/service.py +890 -0
- lightfall_utils-0.2.0/src/lightfall_utils/ipc/trust.py +142 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/theming/manager.py +29 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/threads.py +34 -6
- lightfall_utils-0.2.0/tests/ipc/__init__.py +0 -0
- lightfall_utils-0.2.0/tests/ipc/test_actions.py +239 -0
- lightfall_utils-0.2.0/tests/ipc/test_broker_roundtrip.py +193 -0
- lightfall_utils-0.2.0/tests/ipc/test_capability_channels.py +249 -0
- lightfall_utils-0.2.0/tests/ipc/test_discover_peers.py +129 -0
- lightfall_utils-0.2.0/tests/ipc/test_local_server.py +148 -0
- lightfall_utils-0.2.0/tests/ipc/test_protocol.py +57 -0
- lightfall_utils-0.2.0/tests/ipc/test_service.py +347 -0
- lightfall_utils-0.2.0/tests/ipc/test_trust.py +70 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_threads.py +46 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/.github/workflows/release.yml +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/.gitignore +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/LEGAL.md +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/LICENSE.md +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/__init__.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/ca/__init__.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/ca/context.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/ca/pv.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/caproto_shutdown.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/config/__init__.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/config/layers.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/config/manager.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/log_buffer.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/logging.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/py.typed +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/qt_affinity.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/theming/__init__.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/theming/builtin.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/theming/provider.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/src/lightfall_utils/theming/registry.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/ca_ioc.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/conftest.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_ca_context.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_ca_pv.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_caproto_shutdown.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_config_layers.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_config_manager.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_log_buffer.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_logging.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_package_hygiene.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_qt_affinity.py +0 -0
- {lightfall_utils-0.1.0 → lightfall_utils-0.2.0}/tests/test_theming.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lightfall-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Shared Qt/EPICS infrastructure for ALS control applications: managed threading, loguru logging, semantic theming, layered config, and a caproto/Qt bridge
|
|
5
5
|
Author: ALS Controls Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -8,6 +8,7 @@ License-File: LEGAL.md
|
|
|
8
8
|
License-File: LICENSE.md
|
|
9
9
|
Requires-Python: >=3.11
|
|
10
10
|
Requires-Dist: loguru>=0.7
|
|
11
|
+
Requires-Dist: nats-py>=2.0
|
|
11
12
|
Requires-Dist: pydantic>=2.0
|
|
12
13
|
Requires-Dist: pyside6>=6.6
|
|
13
14
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -15,7 +16,9 @@ Provides-Extra: ca
|
|
|
15
16
|
Requires-Dist: caproto>=1.1; extra == 'ca'
|
|
16
17
|
Provides-Extra: dev
|
|
17
18
|
Requires-Dist: caproto>=1.1; extra == 'dev'
|
|
19
|
+
Requires-Dist: nats-server-bin>=2.14; extra == 'dev'
|
|
18
20
|
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
19
22
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
20
23
|
Requires-Dist: pytest-qt>=4.2; extra == 'dev'
|
|
21
24
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -24,6 +27,8 @@ Provides-Extra: docs
|
|
|
24
27
|
Requires-Dist: myst-parser>=2.0; extra == 'docs'
|
|
25
28
|
Requires-Dist: sphinx-immaterial>=0.12; extra == 'docs'
|
|
26
29
|
Requires-Dist: sphinx<9.0,>=7.0; extra == 'docs'
|
|
30
|
+
Provides-Extra: local-nats
|
|
31
|
+
Requires-Dist: nats-server-bin>=2.14; extra == 'local-nats'
|
|
27
32
|
Provides-Extra: multihomed
|
|
28
33
|
Requires-Dist: netifaces>=0.11; (sys_platform != 'darwin') and extra == 'multihomed'
|
|
29
34
|
Description-Content-Type: text/markdown
|
|
@@ -39,6 +44,7 @@ Extracted from [Lightfall](https://github.com/als-controls/lightfall). Modules:
|
|
|
39
44
|
- `lightfall_utils.qt_affinity` — GUI-thread assertion helpers (`gui_thread_only`)
|
|
40
45
|
- `lightfall_utils.config` — priority-layered YAML config with pydantic validation
|
|
41
46
|
- `lightfall_utils.theming` — semantic design tokens, theme registry/manager, QSS generation
|
|
47
|
+
- `lightfall_utils.ipc` — NATS-backed `IPCService` with a trust handshake, per-session capability channels, discovery and a structured reply protocol; optional `LocalNatsServer` for broker-less development (`pip install lightfall-utils[local-nats]`).
|
|
42
48
|
- `lightfall_utils.ca` — caproto → Qt signal bridge (`SharedContext`, `PV`); requires the `ca` extra
|
|
43
49
|
- `lightfall_utils.caproto_shutdown` — drains caproto's user-callback thread pools cleanly at application shutdown
|
|
44
50
|
|
|
@@ -9,6 +9,7 @@ Extracted from [Lightfall](https://github.com/als-controls/lightfall). Modules:
|
|
|
9
9
|
- `lightfall_utils.qt_affinity` — GUI-thread assertion helpers (`gui_thread_only`)
|
|
10
10
|
- `lightfall_utils.config` — priority-layered YAML config with pydantic validation
|
|
11
11
|
- `lightfall_utils.theming` — semantic design tokens, theme registry/manager, QSS generation
|
|
12
|
+
- `lightfall_utils.ipc` — NATS-backed `IPCService` with a trust handshake, per-session capability channels, discovery and a structured reply protocol; optional `LocalNatsServer` for broker-less development (`pip install lightfall-utils[local-nats]`).
|
|
12
13
|
- `lightfall_utils.ca` — caproto → Qt signal bridge (`SharedContext`, `PV`); requires the `ca` extra
|
|
13
14
|
- `lightfall_utils.caproto_shutdown` — drains caproto's user-callback thread pools cleanly at application shutdown
|
|
14
15
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# IPC: NATS-backed Inter-Process Communication
|
|
2
|
+
|
|
3
|
+
The `lightfall_utils.ipc` module provides a Qt-integrated IPC service built on NATS, with trust handshaking, per-session capability channels, and a structured reply protocol.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
An `IPCService` manages a connection to a NATS broker, publishes and subscribes to a topic tree, handles request/reply patterns for actions, and mints per-session capability tokens for trusted applications. The protocol uses JSON and versioning to ensure compatibility across services.
|
|
8
|
+
|
|
9
|
+
## Topic Structure
|
|
10
|
+
|
|
11
|
+
All user-visible subjects live under a configurable prefix (e.g., `"als.lightfall"`). Well-known discovery uses no prefix:
|
|
12
|
+
|
|
13
|
+
- **Auth handshake:** `{prefix}.auth.request` — app sends `{"app_name", "version"}`, service replies with approval/denial and session token
|
|
14
|
+
- **Meta discovery (no auth required):** `{prefix}.meta.actions` — list registered actions; `{prefix}.meta.events` — list registered events
|
|
15
|
+
- **Peer discovery (no prefix):** `_lightfall.discover` — scatter-gather request/reply to find all live services
|
|
16
|
+
- **Session capability channels:** `{prefix}.session.{token}.>` — per-app capability subject after successful auth; routes to trusted actions with identity attached
|
|
17
|
+
|
|
18
|
+
## Trust Handshake Sequence
|
|
19
|
+
|
|
20
|
+
1. App sends a request to `{prefix}.auth.request` with `{"app_name", "version"}` and a reply subject
|
|
21
|
+
2. Service evaluates trust via `evaluate_trust(app_name)` (delegates to a configured `TrustManager`)
|
|
22
|
+
- If `UNKNOWN`, the service may prompt the user with a `TrustDialog`
|
|
23
|
+
- If `DENIED`, reply with `{"status": "denied", ...}`
|
|
24
|
+
- If `APPROVED`, mint a session capability channel token and reply with `{"status": "approved", "session_token": "...", ...}`
|
|
25
|
+
3. App uses the session token in requests to trusted actions: sends on `{prefix}.session.{token}.action_name` instead of the bare `{prefix}.action_name`
|
|
26
|
+
4. Service attaches identity to the request: `data["_identity"] = {"app_name", "session_token"}`
|
|
27
|
+
|
|
28
|
+
When an app re-authenticates, existing tokens for that app are revoked.
|
|
29
|
+
|
|
30
|
+
## Registering an Action
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from lightfall_utils.ipc import IPCService, ErrorCodes, ok_reply
|
|
34
|
+
|
|
35
|
+
# Initialize the service (connecting to NATS server at construction)
|
|
36
|
+
service = IPCService("nats://localhost:4222", topic_prefix="als.myapp")
|
|
37
|
+
service.start() # Connect to the NATS broker on a background thread
|
|
38
|
+
|
|
39
|
+
# Define app-specific error codes
|
|
40
|
+
app_errors = ErrorCodes("not_found", "validation_failed")
|
|
41
|
+
|
|
42
|
+
# Register an action
|
|
43
|
+
def handle_compute(subject: str, data: dict, reply: str | None) -> None:
|
|
44
|
+
"""Process incoming 'compute' action requests."""
|
|
45
|
+
try:
|
|
46
|
+
value = data.get("value")
|
|
47
|
+
if value is None:
|
|
48
|
+
response = app_errors.reply("validation_failed", "Missing 'value'")
|
|
49
|
+
else:
|
|
50
|
+
result = value * 2
|
|
51
|
+
response = ok_reply(result=result)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
response = app_errors.reply("unknown", str(e))
|
|
54
|
+
|
|
55
|
+
if reply:
|
|
56
|
+
service.reply(reply, response)
|
|
57
|
+
|
|
58
|
+
handle = service.register_action(
|
|
59
|
+
"compute",
|
|
60
|
+
handle_compute,
|
|
61
|
+
description="Double a numeric value",
|
|
62
|
+
schema={
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {"value": {"type": "number"}},
|
|
65
|
+
"required": ["value"],
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Later, unregister with `handle.unregister()`.
|
|
71
|
+
|
|
72
|
+
### Trusted (capability-channel-only) actions
|
|
73
|
+
|
|
74
|
+
Pass `trusted=True` to `register_action()` to make an action reachable only
|
|
75
|
+
through a session capability channel (see "Trust Handshake Sequence" above).
|
|
76
|
+
A request sent to its bare, non-session subject gets a structured `denied`
|
|
77
|
+
error reply instead of reaching the handler; the request must instead be
|
|
78
|
+
sent to `{prefix}.session.{token}.<suffix>` with a token obtained from a
|
|
79
|
+
completed `auth.request` handshake.
|
|
80
|
+
|
|
81
|
+
## Building Auth Responses
|
|
82
|
+
|
|
83
|
+
Use `build_auth_response()` to construct handshake replies:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# Approved with extra fields (e.g., session metadata)
|
|
87
|
+
response = service.build_auth_response(
|
|
88
|
+
approved=True,
|
|
89
|
+
app_name="remote-app",
|
|
90
|
+
extra={"session_id": "s-12345"},
|
|
91
|
+
)
|
|
92
|
+
# Returns: {"status": "approved", "session_token": "...", "session_id": "s-12345", ...}
|
|
93
|
+
|
|
94
|
+
# Denied
|
|
95
|
+
response = service.build_auth_response(
|
|
96
|
+
approved=False,
|
|
97
|
+
reason="Application not registered",
|
|
98
|
+
)
|
|
99
|
+
# Returns: {"status": "denied", "reason": "Application not registered", ...}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Error Codes
|
|
103
|
+
|
|
104
|
+
The protocol defines base error codes that all services understand:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from lightfall_utils.ipc import BASE_ERROR_CODES
|
|
108
|
+
|
|
109
|
+
# BASE_ERROR_CODES = {"busy", "limits", "timeout", "unknown", "denied", "bad_request", "version_mismatch"}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Applications extend this set with their own codes:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from lightfall_utils.ipc import ErrorCodes, error_reply
|
|
116
|
+
|
|
117
|
+
app_codes = ErrorCodes("custom_error_1", "custom_error_2")
|
|
118
|
+
|
|
119
|
+
# Use in error replies
|
|
120
|
+
reply_data = app_codes.reply("custom_error_1", "Something went wrong")
|
|
121
|
+
# Returns: {"status": "error", "code": "custom_error_1", "message": "...", "contract_version": 1}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Peer Discovery
|
|
125
|
+
|
|
126
|
+
Discover all services on the bus:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
def on_peers_found(peers):
|
|
130
|
+
"""Called with a list of {instance_id, display_name, prefix, is_self} dicts."""
|
|
131
|
+
for peer in peers:
|
|
132
|
+
print(f"Peer: {peer['display_name']} at {peer['prefix']}")
|
|
133
|
+
|
|
134
|
+
# Non-blocking; callback runs on the Qt main thread
|
|
135
|
+
service.discover_peers(on_peers_found, timeout_ms=500)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Events
|
|
139
|
+
|
|
140
|
+
Register events for meta-discovery (no subscription created; only for catalog):
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
service.register_event(
|
|
144
|
+
"events.scan.completed",
|
|
145
|
+
description="Emitted when a scan finishes",
|
|
146
|
+
schema={"type": "object", "properties": {"scan_id": {"type": "string"}}},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Later, publish events directly; note that publish() takes the full subject
|
|
150
|
+
# (not a suffix), so use service.topic() to apply the prefix
|
|
151
|
+
service.publish(service.topic("events.scan.completed"), {"scan_id": "s-789"})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Local Development: LocalNatsServer
|
|
155
|
+
|
|
156
|
+
For testing without an external NATS broker, use `LocalNatsServer`:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from lightfall_utils.ipc import LocalNatsServer, IPCService
|
|
160
|
+
|
|
161
|
+
server = LocalNatsServer()
|
|
162
|
+
server.start() # Starts a managed nats-server process
|
|
163
|
+
|
|
164
|
+
service = IPCService("nats://localhost:4222", topic_prefix="als.test")
|
|
165
|
+
service.start() # Connect to the local server
|
|
166
|
+
|
|
167
|
+
# ...
|
|
168
|
+
|
|
169
|
+
service.stop()
|
|
170
|
+
server.stop()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Requires `pip install lightfall-utils[local-nats]`.
|
|
174
|
+
|
|
175
|
+
## Trust Management
|
|
176
|
+
|
|
177
|
+
Use a `TrustManager` to track which applications are trusted:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from lightfall_utils.ipc import TrustManager, TrustState
|
|
181
|
+
|
|
182
|
+
trust = TrustManager()
|
|
183
|
+
service.set_trust_manager(trust)
|
|
184
|
+
|
|
185
|
+
# In auth handler
|
|
186
|
+
state = service.evaluate_trust("remote-app")
|
|
187
|
+
if state == TrustState.UNKNOWN:
|
|
188
|
+
# Prompt user with TrustDialog
|
|
189
|
+
from PySide6.QtWidgets import QDialog
|
|
190
|
+
from lightfall_utils.ipc import TrustDialog
|
|
191
|
+
dialog = TrustDialog("remote-app", app_version="1.0", parent=main_window)
|
|
192
|
+
if dialog.exec() == QDialog.Accepted:
|
|
193
|
+
trust.approve("remote-app")
|
|
194
|
+
else:
|
|
195
|
+
trust.deny("remote-app")
|
|
196
|
+
elif state == TrustState.APPROVED:
|
|
197
|
+
# Grant access
|
|
198
|
+
pass
|
|
199
|
+
elif state == TrustState.DENIED:
|
|
200
|
+
# Reject
|
|
201
|
+
pass
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Protocol Contract
|
|
205
|
+
|
|
206
|
+
All replies carry a `contract_version` field (currently `1`). Applications should check this to detect incompatibilities.
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# `lightfall_utils.ipc` Extraction Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Move Lightfall's NATS IPC layer — `IPCService`, `TrustManager`/`TrustDialog`, the reply protocol, `LocalNatsServer` — into `lightfall-utils` so Camphor (and any NCS app) can be a participant on the bus with the same trust handshake and capability channels.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A *move*, not a rewrite. Four modules come across from `lightfall/src/lightfall/ipc/` and `lightfall/src/lightfall/remote/protocol.py` with their tests; the only code changes are the two Lightfall couplings — `invoke_in_main_thread` (already in `lightfall_utils.threads`) and `build_auth_response`, which stops importing `SessionManager` and takes the app-specific reply fields as a parameter. `get_ipc_service()` (a `ServiceRegistry` lookup) stays in Lightfall. The error-code set becomes extensible per app.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.11+, PySide6, `nats-py>=2.0`, `loguru`; `nats-server-bin` optional for tests and the local broker; pytest + pytest-qt.
|
|
10
|
+
|
|
11
|
+
**Spec:** `C:\Users\rp\workspace\catfish\docs\superpowers\specs\2026-09-10-agent-support-design.md` §3.1, §5.5, §8 step 1. Source of the moved code: `C:\Users\rp\PycharmProjects\ncs\lightfall\src\lightfall\ipc\{service,trust,local_server}.py`, `...\lightfall\remote\protocol.py`, tests under `...\lightfall\tests\ipc\`.
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
- Public names and behaviour are preserved: `IPCService`, `ActionInfo`, `EventInfo`, `TrustManager`, `TrustState`, `TrustDialog`, `LocalNatsServer` and its three error classes, `resolve_nats_binary`, `nats_binary_version`, `probe_nats`, `CONTRACT_VERSION = 1`, `ok_reply`, `error_reply`. Lightfall must be able to re-export them unchanged (its plan, step 2).
|
|
16
|
+
- Wire contract unchanged: subjects, `auth.request` reply shape (`status`, `session_token`, `contract_version`, optional `reason`), `meta.actions`/`meta.events`, discover reply, capability-channel routing, `denied` on bare trusted subjects.
|
|
17
|
+
- No `lightfall.*` import anywhere under `src/lightfall_utils/ipc/`. A hygiene test enforces it.
|
|
18
|
+
- `nats-py>=2.0` becomes a core dependency of lightfall-utils; `nats-server-bin>=2.14` is a new optional extra `local-nats` and a `dev` dependency.
|
|
19
|
+
- Repo conventions: ruff (line length 100), pyright, pytest-qt; commits carry the attribution footer in use in this session.
|
|
20
|
+
- Release as `0.2.0` (tag) when the plan is complete; Lightfall and Camphor floor on it.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### Task 1: Dependencies and the extensible reply protocol
|
|
25
|
+
|
|
26
|
+
**Files:**
|
|
27
|
+
- Modify: `pyproject.toml` (dependencies, optional-dependencies)
|
|
28
|
+
- Create: `src/lightfall_utils/ipc/__init__.py`, `src/lightfall_utils/ipc/protocol.py`
|
|
29
|
+
- Test: `tests/ipc/__init__.py`, `tests/ipc/test_protocol.py`
|
|
30
|
+
|
|
31
|
+
**Interfaces:**
|
|
32
|
+
- Produces: `CONTRACT_VERSION: int = 1`; `BASE_ERROR_CODES: frozenset[str]`; `ok_reply(**fields) -> dict`; `error_reply(code, message, *, extra_codes: Iterable[str] = ()) -> dict`; `ErrorCodes` helper (`ErrorCodes(*app_codes).reply(code, message)`).
|
|
33
|
+
|
|
34
|
+
- [ ] **Step 1: Add the dependencies**
|
|
35
|
+
|
|
36
|
+
In `pyproject.toml`:
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
dependencies = [
|
|
40
|
+
"PySide6>=6.6",
|
|
41
|
+
"loguru>=0.7",
|
|
42
|
+
"pydantic>=2.0",
|
|
43
|
+
"pyyaml>=6.0",
|
|
44
|
+
"nats-py>=2.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
ca = ["caproto>=1.1"]
|
|
49
|
+
local-nats = ["nats-server-bin>=2.14"]
|
|
50
|
+
multihomed = ["netifaces>=0.11; sys_platform != 'darwin'"]
|
|
51
|
+
dev = [
|
|
52
|
+
"caproto>=1.1",
|
|
53
|
+
"nats-server-bin>=2.14",
|
|
54
|
+
"pytest>=8.0",
|
|
55
|
+
"pytest-cov>=4.0",
|
|
56
|
+
"pytest-qt>=4.2",
|
|
57
|
+
"ruff>=0.1",
|
|
58
|
+
"pyright>=1.1",
|
|
59
|
+
]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run: `.venv/Scripts/python.exe -m pip install -e ".[dev]"`
|
|
63
|
+
|
|
64
|
+
- [ ] **Step 2: Write the failing protocol tests**
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# tests/ipc/test_protocol.py
|
|
68
|
+
import pytest
|
|
69
|
+
|
|
70
|
+
from lightfall_utils.ipc.protocol import (
|
|
71
|
+
BASE_ERROR_CODES,
|
|
72
|
+
CONTRACT_VERSION,
|
|
73
|
+
ErrorCodes,
|
|
74
|
+
error_reply,
|
|
75
|
+
ok_reply,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_ok_reply_carries_the_contract_version():
|
|
80
|
+
assert ok_reply(status="ok", value=3) == {"status": "ok", "value": 3, "contract_version": 1}
|
|
81
|
+
assert CONTRACT_VERSION == 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_error_reply_shape_for_a_base_code():
|
|
85
|
+
assert error_reply("denied", "nope") == {
|
|
86
|
+
"status": "error", "code": "denied", "message": "nope", "contract_version": 1,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_lightfall_s_codes_are_all_in_the_base_set():
|
|
91
|
+
# The seven codes Lightfall's remote-control contract v1 documents.
|
|
92
|
+
assert {"busy", "limits", "timeout", "unknown", "denied", "bad_request",
|
|
93
|
+
"version_mismatch"} <= BASE_ERROR_CODES
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_an_unknown_code_is_a_programming_error():
|
|
97
|
+
with pytest.raises(ValueError):
|
|
98
|
+
error_reply("not_a_code", "x")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_an_app_extends_the_set_without_touching_the_base():
|
|
102
|
+
codes = ErrorCodes("too_large", "confirmation_required")
|
|
103
|
+
assert codes.reply("too_large", "1.2 MiB > 1 MiB")["code"] == "too_large"
|
|
104
|
+
assert codes.reply("denied", "bare subject")["code"] == "denied" # base still valid
|
|
105
|
+
with pytest.raises(ValueError):
|
|
106
|
+
codes.reply("busy_beaver", "x")
|
|
107
|
+
assert "too_large" not in BASE_ERROR_CODES
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- [ ] **Step 3: Run to verify failure**
|
|
111
|
+
|
|
112
|
+
Run: `.venv/Scripts/python.exe -m pytest tests/ipc/test_protocol.py -q`
|
|
113
|
+
Expected: FAIL — `ModuleNotFoundError: lightfall_utils.ipc`
|
|
114
|
+
|
|
115
|
+
- [ ] **Step 4: Implement `protocol.py`**
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# src/lightfall_utils/ipc/protocol.py
|
|
119
|
+
"""Reply protocol for NCS IPC contracts (v1).
|
|
120
|
+
|
|
121
|
+
Every reply -- success or error -- carries ``contract_version`` so a client
|
|
122
|
+
can detect a mismatch. Errors are structured: ``{status: "error", code,
|
|
123
|
+
message, contract_version}``. The base code set is Lightfall's remote-control
|
|
124
|
+
contract; an application adds its own with :class:`ErrorCodes` rather than by
|
|
125
|
+
editing this set, so two apps on one bus never disagree about a base code.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
from __future__ import annotations
|
|
129
|
+
|
|
130
|
+
from collections.abc import Iterable
|
|
131
|
+
from typing import Any
|
|
132
|
+
|
|
133
|
+
__all__ = ["BASE_ERROR_CODES", "CONTRACT_VERSION", "ErrorCodes", "error_reply", "ok_reply"]
|
|
134
|
+
|
|
135
|
+
CONTRACT_VERSION = 1
|
|
136
|
+
|
|
137
|
+
BASE_ERROR_CODES = frozenset(
|
|
138
|
+
{"busy", "limits", "timeout", "unknown", "denied", "bad_request", "version_mismatch"}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def ok_reply(**fields: Any) -> dict:
|
|
143
|
+
"""A success reply carrying ``contract_version``."""
|
|
144
|
+
return {**fields, "contract_version": CONTRACT_VERSION}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def error_reply(code: str, message: str, *, extra_codes: Iterable[str] = ()) -> dict:
|
|
148
|
+
"""A structured error reply. ``code`` must be a base code or one of ``extra_codes``."""
|
|
149
|
+
if code not in BASE_ERROR_CODES and code not in set(extra_codes):
|
|
150
|
+
raise ValueError(f"Unknown error code: {code!r}")
|
|
151
|
+
return {
|
|
152
|
+
"status": "error",
|
|
153
|
+
"code": code,
|
|
154
|
+
"message": message,
|
|
155
|
+
"contract_version": CONTRACT_VERSION,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ErrorCodes:
|
|
160
|
+
"""An application's error vocabulary: the base set plus its own codes."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, *app_codes: str) -> None:
|
|
163
|
+
self.codes: frozenset[str] = BASE_ERROR_CODES | frozenset(app_codes)
|
|
164
|
+
|
|
165
|
+
def reply(self, code: str, message: str) -> dict:
|
|
166
|
+
return error_reply(code, message, extra_codes=self.codes)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`src/lightfall_utils/ipc/__init__.py` for now:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
"""NATS-backed IPC for NCS applications: service, trust handshake, protocol, local broker."""
|
|
173
|
+
|
|
174
|
+
from lightfall_utils.ipc.protocol import (
|
|
175
|
+
BASE_ERROR_CODES,
|
|
176
|
+
CONTRACT_VERSION,
|
|
177
|
+
ErrorCodes,
|
|
178
|
+
error_reply,
|
|
179
|
+
ok_reply,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
__all__ = ["BASE_ERROR_CODES", "CONTRACT_VERSION", "ErrorCodes", "error_reply", "ok_reply"]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- [ ] **Step 5: Run to verify pass, lint, commit**
|
|
186
|
+
|
|
187
|
+
Run: `.venv/Scripts/python.exe -m pytest tests/ipc -q && .venv/Scripts/python.exe -m ruff check src tests`
|
|
188
|
+
Expected: 5 passed, lint clean.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
git add pyproject.toml src/lightfall_utils/ipc tests/ipc
|
|
192
|
+
git commit -m "feat(ipc): reply protocol with an app-extensible error vocabulary; nats-py dependency"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Task 2: Move `TrustManager` / `TrustDialog`
|
|
198
|
+
|
|
199
|
+
**Files:**
|
|
200
|
+
- Create: `src/lightfall_utils/ipc/trust.py` (from `lightfall/src/lightfall/ipc/trust.py`, 144 lines)
|
|
201
|
+
- Test: `tests/ipc/test_trust.py` (from `lightfall/tests/ipc/test_trust.py`)
|
|
202
|
+
|
|
203
|
+
**Interfaces:**
|
|
204
|
+
- Produces: `TrustState` (`UNKNOWN`, `APPROVED`, `DENIED`), `TrustManager` (`check`, `approve`, `deny`, `revoke`, `clear`, `list_approved`/whatever the source exposes — preserved verbatim), `TrustDialog(app_name, app_version, parent=None)`.
|
|
205
|
+
|
|
206
|
+
- [ ] **Step 1: Copy the source module and its test verbatim**
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cp ../lightfall/src/lightfall/ipc/trust.py src/lightfall_utils/ipc/trust.py
|
|
210
|
+
cp ../lightfall/tests/ipc/test_trust.py tests/ipc/test_trust.py
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- [ ] **Step 2: Fix imports**
|
|
214
|
+
|
|
215
|
+
In `tests/ipc/test_trust.py`: `from lightfall.ipc.trust import ...` → `from lightfall_utils.ipc.trust import ...`. `trust.py` itself imports only stdlib and `PySide6.QtWidgets`; verify with `grep -n "^from lightfall" src/lightfall_utils/ipc/trust.py` → no output.
|
|
216
|
+
|
|
217
|
+
- [ ] **Step 3: Run the moved tests**
|
|
218
|
+
|
|
219
|
+
Run: `.venv/Scripts/python.exe -m pytest tests/ipc/test_trust.py -q`
|
|
220
|
+
Expected: all pass (the module is unchanged).
|
|
221
|
+
|
|
222
|
+
- [ ] **Step 4: Export and commit**
|
|
223
|
+
|
|
224
|
+
Add to `src/lightfall_utils/ipc/__init__.py`: `from lightfall_utils.ipc.trust import TrustDialog, TrustManager, TrustState` and extend `__all__`.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
git add src/lightfall_utils/ipc/trust.py src/lightfall_utils/ipc/__init__.py tests/ipc/test_trust.py
|
|
228
|
+
git commit -m "feat(ipc): move TrustManager/TrustDialog from Lightfall"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Task 3: Move `LocalNatsServer`
|
|
234
|
+
|
|
235
|
+
**Files:**
|
|
236
|
+
- Create: `src/lightfall_utils/ipc/local_server.py` (from `lightfall/src/lightfall/ipc/local_server.py`, 175 lines)
|
|
237
|
+
- Test: `tests/ipc/test_local_server.py` (from Lightfall)
|
|
238
|
+
|
|
239
|
+
**Interfaces:**
|
|
240
|
+
- Produces: `LocalNatsServer(port=4222, host="127.0.0.1")` with `start(timeout_s)`, `stop()`, `url`; `LocalNatsServerError`, `NatsBinaryNotFoundError`, `NatsPortInUseError`, `NatsReadinessTimeoutError`; `resolve_nats_binary()`, `nats_binary_version(path)`, `probe_nats(host, port, timeout)`.
|
|
241
|
+
|
|
242
|
+
- [ ] **Step 1: Copy module and test; fix imports**
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
cp ../lightfall/src/lightfall/ipc/local_server.py src/lightfall_utils/ipc/local_server.py
|
|
246
|
+
cp ../lightfall/tests/ipc/test_local_server.py tests/ipc/test_local_server.py
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`local_server.py` imports stdlib + `loguru` only. In the test replace `lightfall.ipc` with `lightfall_utils.ipc` (both the `from lightfall.ipc import local_server` and `from lightfall.ipc.local_server import (...)` forms).
|
|
250
|
+
|
|
251
|
+
- [ ] **Step 2: Run**
|
|
252
|
+
|
|
253
|
+
Run: `.venv/Scripts/python.exe -m pytest tests/ipc/test_local_server.py -q`
|
|
254
|
+
Expected: pass (`nats-server-bin` is in `dev`, so the bundled binary resolves).
|
|
255
|
+
|
|
256
|
+
- [ ] **Step 3: Export and commit**
|
|
257
|
+
|
|
258
|
+
Add the class, errors and three functions to `__init__.py` exports.
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
git add src/lightfall_utils/ipc/local_server.py src/lightfall_utils/ipc/__init__.py tests/ipc/test_local_server.py
|
|
262
|
+
git commit -m "feat(ipc): move LocalNatsServer from Lightfall"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### Task 4: Move `IPCService`, decoupled from Lightfall
|
|
268
|
+
|
|
269
|
+
**Files:**
|
|
270
|
+
- Create: `src/lightfall_utils/ipc/service.py` (from `lightfall/src/lightfall/ipc/service.py`, 932 lines)
|
|
271
|
+
- Test: `tests/ipc/test_service.py`, `tests/ipc/test_actions.py`, `tests/ipc/test_capability_channels.py`, `tests/ipc/test_discover_peers.py` (from Lightfall's `tests/ipc/`)
|
|
272
|
+
|
|
273
|
+
**Interfaces:**
|
|
274
|
+
- Produces: `IPCService(nats_url, topic_prefix="", parent=None)` with every existing method; **changed**: `build_auth_response(*, approved: bool, reason: str = "", app_name: str | None = None, extra: Mapping[str, Any] | None = None) -> dict` — no `session`/`tiled_url`; the approved reply is `{"status": "approved", **extra, "contract_version": 1, "session_token": ...}`. **Removed here**: `get_ipc_service()` (Lightfall keeps it; it needs `ServiceRegistry`).
|
|
275
|
+
- Consumes: `lightfall_utils.threads.invoke_in_main_thread`; `lightfall_utils.ipc.trust`; `lightfall_utils.ipc.protocol` (`CONTRACT_VERSION`, `error_reply`).
|
|
276
|
+
|
|
277
|
+
- [ ] **Step 1: Copy the module and the four tests**
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
cp ../lightfall/src/lightfall/ipc/service.py src/lightfall_utils/ipc/service.py
|
|
281
|
+
for t in test_service test_actions test_capability_channels test_discover_peers; do
|
|
282
|
+
cp ../lightfall/tests/ipc/$t.py tests/ipc/$t.py
|
|
283
|
+
done
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- [ ] **Step 2: Rewrite the imports in `service.py`**
|
|
287
|
+
|
|
288
|
+
Replace:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from lightfall.ipc.trust import TrustManager, TrustState
|
|
292
|
+
from lightfall.remote.protocol import CONTRACT_VERSION, error_reply
|
|
293
|
+
from lightfall.utils.threads import invoke_in_main_thread
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
with:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from lightfall_utils.ipc.protocol import CONTRACT_VERSION, error_reply
|
|
300
|
+
from lightfall_utils.ipc.trust import TrustManager, TrustState
|
|
301
|
+
from lightfall_utils.threads import invoke_in_main_thread
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Delete `get_ipc_service` (lines 923–932 of the source) and drop it from `__all__`.
|
|
305
|
+
|
|
306
|
+
- [ ] **Step 3: Decouple `build_auth_response`**
|
|
307
|
+
|
|
308
|
+
Replace the method body (source lines 329–395) with:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
def build_auth_response(
|
|
312
|
+
self,
|
|
313
|
+
*,
|
|
314
|
+
approved: bool,
|
|
315
|
+
reason: str = "",
|
|
316
|
+
app_name: str | None = None,
|
|
317
|
+
extra: Mapping[str, Any] | None = None,
|
|
318
|
+
) -> dict:
|
|
319
|
+
"""Build the reply to an ``auth.request`` handshake.
|
|
320
|
+
|
|
321
|
+
``extra`` carries the application's own approved-reply fields --
|
|
322
|
+
Lightfall adds ``tiled_token``, ``tiled_url`` and ``session_id``; an
|
|
323
|
+
app with nothing to add passes none. When ``app_name`` is given and the
|
|
324
|
+
request is approved, a session capability channel is minted for it and
|
|
325
|
+
its token returned as ``session_token``. A denial carries ``reason``
|
|
326
|
+
when non-empty.
|
|
327
|
+
"""
|
|
328
|
+
if approved:
|
|
329
|
+
response: dict = {"status": "approved", **(dict(extra) if extra else {})}
|
|
330
|
+
response["contract_version"] = CONTRACT_VERSION
|
|
331
|
+
if app_name:
|
|
332
|
+
response["session_token"] = self.mint_session_channel(app_name)
|
|
333
|
+
return response
|
|
334
|
+
response = {"status": "denied", "contract_version": CONTRACT_VERSION}
|
|
335
|
+
if reason:
|
|
336
|
+
response["reason"] = reason
|
|
337
|
+
return response
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Add `from collections.abc import Mapping` to the imports if missing.
|
|
341
|
+
|
|
342
|
+
- [ ] **Step 4: Fix the moved tests**
|
|
343
|
+
|
|
344
|
+
- Every `from lightfall.ipc.service import` → `from lightfall_utils.ipc.service import`; `from lightfall.ipc.trust` → `from lightfall_utils.ipc.trust`.
|
|
345
|
+
- `tests/ipc/test_service.py` around source line 285 patches `lightfall.auth.session.SessionManager` to test the Tiled fields of `build_auth_response`. Replace that test with one for the new signature:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
def test_build_auth_response_approved_mints_a_channel_and_carries_extra_fields():
|
|
349
|
+
svc = IPCService("nats://unused", topic_prefix="als.test")
|
|
350
|
+
svc.set_trust_manager(TrustManager())
|
|
351
|
+
reply = svc.build_auth_response(
|
|
352
|
+
approved=True, app_name="camphor-mcp", extra={"tiled_url": "https://t"}
|
|
353
|
+
)
|
|
354
|
+
assert reply["status"] == "approved"
|
|
355
|
+
assert reply["tiled_url"] == "https://t"
|
|
356
|
+
assert reply["contract_version"] == 1
|
|
357
|
+
assert isinstance(reply["session_token"], str) and len(reply["session_token"]) > 20
|
|
358
|
+
assert svc.session_channel_count == 1
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_build_auth_response_denied_carries_reason_only_when_given():
|
|
362
|
+
svc = IPCService("nats://unused")
|
|
363
|
+
assert svc.build_auth_response(approved=False) == {"status": "denied", "contract_version": 1}
|
|
364
|
+
assert svc.build_auth_response(approved=False, reason="timeout")["reason"] == "timeout"
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
- `tests/ipc/test_capability_channels.py` imports `lightfall.auth.session` at source line ~235 for a logout-teardown test. That test stays in Lightfall (it tests Lightfall's `_wire_session_trust`); delete it from the copy here. Every other test in the file exercises `IPCService` alone and stays.
|
|
368
|
+
- Any test that calls `get_ipc_service` moves back to Lightfall (delete from the copy).
|
|
369
|
+
|
|
370
|
+
- [ ] **Step 5: Hygiene test**
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
# tests/ipc/test_no_lightfall_imports.py
|
|
374
|
+
import pathlib
|
|
375
|
+
import re
|
|
376
|
+
|
|
377
|
+
PKG = pathlib.Path(__file__).resolve().parents[2] / "src" / "lightfall_utils" / "ipc"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_ipc_package_imports_nothing_from_lightfall():
|
|
381
|
+
offenders = []
|
|
382
|
+
for path in PKG.glob("*.py"):
|
|
383
|
+
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
384
|
+
if re.match(r"\s*(from|import)\s+lightfall(\.|\s|$)", line):
|
|
385
|
+
offenders.append(f"{path.name}:{n}: {line.strip()}")
|
|
386
|
+
assert offenders == []
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- [ ] **Step 6: Run everything**
|
|
390
|
+
|
|
391
|
+
Run: `.venv/Scripts/python.exe -m pytest tests/ipc -q -p no:cacheprovider`
|
|
392
|
+
Expected: all pass. (`test_discover_peers` and the integration-style tests in `test_actions` start a real `nats-server` via `LocalNatsServer`; `nats-server-bin` is in `dev`.)
|
|
393
|
+
|
|
394
|
+
Run: `.venv/Scripts/python.exe -m ruff check src tests && .venv/Scripts/python.exe -m pyright src/lightfall_utils/ipc`
|
|
395
|
+
Expected: clean.
|
|
396
|
+
|
|
397
|
+
- [ ] **Step 7: Export and commit**
|
|
398
|
+
|
|
399
|
+
`__init__.py` final export list: `IPCService, ActionInfo, EventInfo, TrustDialog, TrustManager, TrustState, LocalNatsServer, LocalNatsServerError, NatsBinaryNotFoundError, NatsPortInUseError, NatsReadinessTimeoutError, resolve_nats_binary, nats_binary_version, probe_nats, BASE_ERROR_CODES, CONTRACT_VERSION, ErrorCodes, error_reply, ok_reply`.
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
git add src/lightfall_utils/ipc tests/ipc
|
|
403
|
+
git commit -m "feat(ipc): move IPCService from Lightfall; app-neutral auth reply"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
### Task 5: Docs, README, release
|
|
409
|
+
|
|
410
|
+
**Files:**
|
|
411
|
+
- Modify: `README.md` (feature list + one paragraph), `docs/` (new page `docs/ipc.md` if the docs tree has per-module pages; otherwise a section in the existing API page)
|
|
412
|
+
- Modify: `tests/test_package_hygiene.py` if it enumerates public modules
|
|
413
|
+
|
|
414
|
+
- [ ] **Step 1: Document**
|
|
415
|
+
|
|
416
|
+
In `README.md` add a bullet under the feature list: "**IPC** — NATS-backed `IPCService` with a trust handshake, per-session capability channels, discovery and a structured reply protocol; optional `LocalNatsServer` for broker-less development (`pip install lightfall-utils[local-nats]`)." Add `docs/ipc.md` containing: the subject shapes (`{prefix}.auth.request`, `{prefix}.meta.actions/events`, `{prefix}.session.{token}.<suffix>`, discover), the handshake sequence, `build_auth_response(extra=...)` for app fields, `ErrorCodes` for app vocabularies, and a 20-line "register an action" example (adapted from Lightfall's `ipc-architecture.md` "Registering a New Action", with `ipc.reply(reply, ok_reply(status="ok", ...))`).
|
|
417
|
+
|
|
418
|
+
- [ ] **Step 2: Full suite, tag**
|
|
419
|
+
|
|
420
|
+
Run: `.venv/Scripts/python.exe -m pytest -q -p no:cacheprovider`
|
|
421
|
+
Expected: all green (the existing suites plus `tests/ipc`).
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
git add README.md docs
|
|
425
|
+
git commit -m "docs: lightfall_utils.ipc"
|
|
426
|
+
git tag -a v0.2.0 -m "lightfall-utils 0.2.0: NATS IPC layer extracted from Lightfall"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
(Do **not** push; Ron decides when.)
|