pacerelle 0.1.0a0__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.
@@ -0,0 +1,56 @@
1
+ # Secrets and local env
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ !.env.dev.example
6
+
7
+ # Node / TypeScript
8
+ node_modules/
9
+ dist/
10
+ build/
11
+ .turbo/
12
+ .vite/
13
+ coverage/
14
+ playwright-report/
15
+ test-results/
16
+ *.tsbuildinfo
17
+
18
+ # Go
19
+ bin/
20
+ *.test
21
+ *.out
22
+ vendor/
23
+ gateway/gateway
24
+ mcp-bridge/mcp-bridge
25
+
26
+ # Python
27
+ .venv/
28
+ __pycache__/
29
+ *.py[cod]
30
+ .pytest_cache/
31
+ .ruff_cache/
32
+ .mypy_cache/
33
+ *.egg-info/
34
+
35
+ # Databases and local state
36
+ *.sqlite
37
+ *.sqlite3
38
+ tmp/
39
+ temp/
40
+
41
+ # Local proof-of-concept workspace
42
+ poc-workspace/
43
+ target/
44
+
45
+ # IDE / OS
46
+ .idea/
47
+ .vscode/
48
+ .DS_Store
49
+ Thumbs.db
50
+
51
+ # Logs
52
+ *.log
53
+ npm-debug.log*
54
+ yarn-debug.log*
55
+ yarn-error.log*
56
+ pnpm-debug.log*
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pacerelle
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,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: pacerelle
3
+ Version: 0.1.0a0
4
+ Summary: Python SDK for Pacerelle encrypted local agent relays.
5
+ Project-URL: Homepage, https://pacerelle.com
6
+ Project-URL: Documentation, https://pacerelle.com
7
+ Author: Pacerelle
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,ai,e2ee,local-agents,mcp,websocket
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: cryptography>=42.0.0
20
+ Requires-Dist: websockets>=12.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Pacerelle Python SDK
24
+
25
+ Python agent client for Pacerelle encrypted local agent relays.
26
+
27
+ Use this SDK to connect a local Python process to Pacerelle, receive messages,
28
+ reply to conversations, drive widgets, and return encrypted files or media.
29
+
30
+ ```bash
31
+ pip install pacerelle
32
+ ```
33
+
34
+ > Alpha release: APIs and native E2EE packaging may change before the first
35
+ > stable release.
36
+
37
+ ## Before You Run
38
+
39
+ Create an agent in Pacerelle. The confirmation panel shows both
40
+ `Identifiant de l'agent` and `Jeton d'authentification`. Use
41
+ `Copier la configuration .env` to copy the required variables.
42
+
43
+ ```bash
44
+ export PACERELLE_AGENT_ID="agent-id"
45
+ export PACERELLE_AGENT_TOKEN="agent-token"
46
+ ```
47
+
48
+ On Windows PowerShell:
49
+
50
+ ```powershell
51
+ $env:PACERELLE_AGENT_ID = "agent-id"
52
+ $env:PACERELLE_AGENT_TOKEN = "agent-token"
53
+ ```
54
+
55
+ Published packages connect to the Pacerelle API by default. Local source builds
56
+ default to `http://localhost:8080` for development.
57
+
58
+ ## Minimal Echo Agent
59
+
60
+ ```python
61
+ import asyncio
62
+ import os
63
+
64
+ from pacerelle import AgentGatewayClient
65
+
66
+ client = AgentGatewayClient(
67
+ token=os.environ["PACERELLE_AGENT_TOKEN"],
68
+ agent_id=os.environ["PACERELLE_AGENT_ID"],
69
+ e2ee=True,
70
+ )
71
+
72
+
73
+ async def handle(message, agent):
74
+ await agent.send_message(
75
+ conversation_id=message.conversation_id,
76
+ to=message.from_id,
77
+ reply_to_message_id=message.id,
78
+ text=f"Received: {message.text}",
79
+ )
80
+
81
+
82
+ client.on_message(handle)
83
+ asyncio.run(client.connect())
84
+ ```
85
+
86
+ ## Incoming Messages
87
+
88
+ The handler receives an `AgentMessage`:
89
+
90
+ ```python
91
+ async def handle(message, agent):
92
+ print(message.id)
93
+ print(message.conversation_id)
94
+ print(message.from_id)
95
+ print(message.text)
96
+ print(message.attachments)
97
+ print(message.widget_response)
98
+ ```
99
+
100
+ Use `message.from_id` as the `to` value when replying to the user.
101
+
102
+ ## Sending Messages And Replies
103
+
104
+ Send a normal message:
105
+
106
+ ```python
107
+ await agent.send_message(
108
+ conversation_id=message.conversation_id,
109
+ to=message.from_id,
110
+ text="I can help with that.",
111
+ )
112
+ ```
113
+
114
+ Reply to a specific user message:
115
+
116
+ ```python
117
+ await agent.send_message(
118
+ conversation_id=message.conversation_id,
119
+ to=message.from_id,
120
+ reply_to_message_id=message.id,
121
+ text="Replying to your last message.",
122
+ )
123
+ ```
124
+
125
+ ## Running Your Own Agent Logic
126
+
127
+ ```python
128
+ async def handle(message, agent):
129
+ result = await run_my_agent(message.text)
130
+
131
+ await agent.send_message(
132
+ conversation_id=message.conversation_id,
133
+ to=message.from_id,
134
+ reply_to_message_id=message.id,
135
+ text=result,
136
+ )
137
+ ```
138
+
139
+ ## Widgets
140
+
141
+ Widgets are sent as encrypted conversation messages. Each method returns the
142
+ widget id. User answers arrive later as `message.widget_response`.
143
+
144
+ ### Confirm
145
+
146
+ ```python
147
+ await agent.send_confirm_widget(
148
+ conversation_id=message.conversation_id,
149
+ to=message.from_id,
150
+ widget_id="confirm-delete",
151
+ title="Delete file?",
152
+ body="This cannot be undone.",
153
+ danger=True,
154
+ labels={"yes": "Delete", "no": "Cancel"},
155
+ )
156
+ ```
157
+
158
+ Handle the answer:
159
+
160
+ ```python
161
+ if message.widget_response and message.widget_response.ref == "confirm-delete":
162
+ if message.widget_response.cancelled:
163
+ return
164
+ if message.widget_response.value is True:
165
+ await agent.send_message(
166
+ conversation_id=message.conversation_id,
167
+ to=message.from_id,
168
+ text="Confirmed.",
169
+ )
170
+ ```
171
+
172
+ ### Choice
173
+
174
+ ```python
175
+ await agent.send_choice_widget(
176
+ conversation_id=message.conversation_id,
177
+ to=message.from_id,
178
+ widget_id="choose-format",
179
+ title="Choose a format",
180
+ options=[
181
+ {"id": "pdf", "label": "PDF"},
182
+ {"id": "csv", "label": "CSV"},
183
+ ],
184
+ multi=False,
185
+ )
186
+ ```
187
+
188
+ ### Permission
189
+
190
+ ```python
191
+ await agent.send_permission_widget(
192
+ conversation_id=message.conversation_id,
193
+ to=message.from_id,
194
+ widget_id="permission-files",
195
+ title="Allow file access?",
196
+ body="The agent needs access to selected files.",
197
+ scopes=["once", "session"],
198
+ )
199
+ ```
200
+
201
+ ### Form
202
+
203
+ ```python
204
+ await agent.send_form_widget(
205
+ conversation_id=message.conversation_id,
206
+ to=message.from_id,
207
+ widget_id="profile-form",
208
+ title="Complete profile",
209
+ submitLabel="Save",
210
+ fields=[
211
+ {"name": "email", "label": "Email", "type": "email", "required": True},
212
+ {"name": "notes", "label": "Notes", "type": "textarea"},
213
+ ],
214
+ )
215
+ ```
216
+
217
+ ### Progress
218
+
219
+ ```python
220
+ progress_id = await agent.send_progress_widget(
221
+ conversation_id=message.conversation_id,
222
+ to=message.from_id,
223
+ widget_id="import-progress",
224
+ title="Importing files",
225
+ value=10,
226
+ max=100,
227
+ cancellable=True,
228
+ )
229
+ ```
230
+
231
+ Update it:
232
+
233
+ ```python
234
+ await agent.send_widget_update(
235
+ conversation_id=message.conversation_id,
236
+ to=message.from_id,
237
+ ref=progress_id,
238
+ spec={"value": 65, "body": "Almost done"},
239
+ )
240
+ ```
241
+
242
+ ### File Picker
243
+
244
+ ```python
245
+ await agent.send_file_picker_widget(
246
+ conversation_id=message.conversation_id,
247
+ to=message.from_id,
248
+ widget_id="pick-files",
249
+ title="Choose files",
250
+ multiple=True,
251
+ accept=[".pdf", "image/*"],
252
+ max_files=5,
253
+ )
254
+ ```
255
+
256
+ ### Date And Time
257
+
258
+ ```python
259
+ await agent.send_datetime_widget(
260
+ conversation_id=message.conversation_id,
261
+ to=message.from_id,
262
+ widget_id="schedule",
263
+ title="Pick a meeting time",
264
+ mode="datetime",
265
+ min="2026-05-21T09:00:00",
266
+ )
267
+ ```
268
+
269
+ ## Files And Media
270
+
271
+ `send_file` and `send_media` encrypt bytes locally with AES-GCM, upload only
272
+ ciphertext to `/agent/blobs`, then send the attachment key and IV inside the
273
+ E2EE message payload.
274
+
275
+ ```python
276
+ await agent.send_file(
277
+ conversation_id=message.conversation_id,
278
+ to=message.from_id,
279
+ reply_to_message_id=message.id,
280
+ text="Here is the report.",
281
+ name="report.txt",
282
+ mime="text/plain",
283
+ data=b"private report",
284
+ )
285
+ ```
286
+
287
+ Media adds optional dimensions or duration:
288
+
289
+ ```python
290
+ await agent.send_media(
291
+ conversation_id=message.conversation_id,
292
+ to=message.from_id,
293
+ text="Preview attached.",
294
+ name="chart.png",
295
+ mime="image/png",
296
+ data=png_bytes,
297
+ width=1200,
298
+ height=800,
299
+ )
300
+ ```
301
+
302
+ ## Encryption
303
+
304
+ When `e2ee=True`, the SDK encrypts and decrypts messages locally before they
305
+ leave your machine. On connect, the client publishes the agent pre-key bundle,
306
+ establishes encrypted sessions for conversations, and keeps message contents
307
+ opaque to the relay.
308
+
309
+ Use `e2ee=False` only for local debugging or non-encrypted transports.
310
+
311
+ ## MCP
312
+
313
+ This package is the Python SDK for building agents. The MCP server is
314
+ distributed separately:
315
+
316
+ ```bash
317
+ npx -y @pacerelle/mcp-server
318
+ ```
@@ -0,0 +1,296 @@
1
+ # Pacerelle Python SDK
2
+
3
+ Python agent client for Pacerelle encrypted local agent relays.
4
+
5
+ Use this SDK to connect a local Python process to Pacerelle, receive messages,
6
+ reply to conversations, drive widgets, and return encrypted files or media.
7
+
8
+ ```bash
9
+ pip install pacerelle
10
+ ```
11
+
12
+ > Alpha release: APIs and native E2EE packaging may change before the first
13
+ > stable release.
14
+
15
+ ## Before You Run
16
+
17
+ Create an agent in Pacerelle. The confirmation panel shows both
18
+ `Identifiant de l'agent` and `Jeton d'authentification`. Use
19
+ `Copier la configuration .env` to copy the required variables.
20
+
21
+ ```bash
22
+ export PACERELLE_AGENT_ID="agent-id"
23
+ export PACERELLE_AGENT_TOKEN="agent-token"
24
+ ```
25
+
26
+ On Windows PowerShell:
27
+
28
+ ```powershell
29
+ $env:PACERELLE_AGENT_ID = "agent-id"
30
+ $env:PACERELLE_AGENT_TOKEN = "agent-token"
31
+ ```
32
+
33
+ Published packages connect to the Pacerelle API by default. Local source builds
34
+ default to `http://localhost:8080` for development.
35
+
36
+ ## Minimal Echo Agent
37
+
38
+ ```python
39
+ import asyncio
40
+ import os
41
+
42
+ from pacerelle import AgentGatewayClient
43
+
44
+ client = AgentGatewayClient(
45
+ token=os.environ["PACERELLE_AGENT_TOKEN"],
46
+ agent_id=os.environ["PACERELLE_AGENT_ID"],
47
+ e2ee=True,
48
+ )
49
+
50
+
51
+ async def handle(message, agent):
52
+ await agent.send_message(
53
+ conversation_id=message.conversation_id,
54
+ to=message.from_id,
55
+ reply_to_message_id=message.id,
56
+ text=f"Received: {message.text}",
57
+ )
58
+
59
+
60
+ client.on_message(handle)
61
+ asyncio.run(client.connect())
62
+ ```
63
+
64
+ ## Incoming Messages
65
+
66
+ The handler receives an `AgentMessage`:
67
+
68
+ ```python
69
+ async def handle(message, agent):
70
+ print(message.id)
71
+ print(message.conversation_id)
72
+ print(message.from_id)
73
+ print(message.text)
74
+ print(message.attachments)
75
+ print(message.widget_response)
76
+ ```
77
+
78
+ Use `message.from_id` as the `to` value when replying to the user.
79
+
80
+ ## Sending Messages And Replies
81
+
82
+ Send a normal message:
83
+
84
+ ```python
85
+ await agent.send_message(
86
+ conversation_id=message.conversation_id,
87
+ to=message.from_id,
88
+ text="I can help with that.",
89
+ )
90
+ ```
91
+
92
+ Reply to a specific user message:
93
+
94
+ ```python
95
+ await agent.send_message(
96
+ conversation_id=message.conversation_id,
97
+ to=message.from_id,
98
+ reply_to_message_id=message.id,
99
+ text="Replying to your last message.",
100
+ )
101
+ ```
102
+
103
+ ## Running Your Own Agent Logic
104
+
105
+ ```python
106
+ async def handle(message, agent):
107
+ result = await run_my_agent(message.text)
108
+
109
+ await agent.send_message(
110
+ conversation_id=message.conversation_id,
111
+ to=message.from_id,
112
+ reply_to_message_id=message.id,
113
+ text=result,
114
+ )
115
+ ```
116
+
117
+ ## Widgets
118
+
119
+ Widgets are sent as encrypted conversation messages. Each method returns the
120
+ widget id. User answers arrive later as `message.widget_response`.
121
+
122
+ ### Confirm
123
+
124
+ ```python
125
+ await agent.send_confirm_widget(
126
+ conversation_id=message.conversation_id,
127
+ to=message.from_id,
128
+ widget_id="confirm-delete",
129
+ title="Delete file?",
130
+ body="This cannot be undone.",
131
+ danger=True,
132
+ labels={"yes": "Delete", "no": "Cancel"},
133
+ )
134
+ ```
135
+
136
+ Handle the answer:
137
+
138
+ ```python
139
+ if message.widget_response and message.widget_response.ref == "confirm-delete":
140
+ if message.widget_response.cancelled:
141
+ return
142
+ if message.widget_response.value is True:
143
+ await agent.send_message(
144
+ conversation_id=message.conversation_id,
145
+ to=message.from_id,
146
+ text="Confirmed.",
147
+ )
148
+ ```
149
+
150
+ ### Choice
151
+
152
+ ```python
153
+ await agent.send_choice_widget(
154
+ conversation_id=message.conversation_id,
155
+ to=message.from_id,
156
+ widget_id="choose-format",
157
+ title="Choose a format",
158
+ options=[
159
+ {"id": "pdf", "label": "PDF"},
160
+ {"id": "csv", "label": "CSV"},
161
+ ],
162
+ multi=False,
163
+ )
164
+ ```
165
+
166
+ ### Permission
167
+
168
+ ```python
169
+ await agent.send_permission_widget(
170
+ conversation_id=message.conversation_id,
171
+ to=message.from_id,
172
+ widget_id="permission-files",
173
+ title="Allow file access?",
174
+ body="The agent needs access to selected files.",
175
+ scopes=["once", "session"],
176
+ )
177
+ ```
178
+
179
+ ### Form
180
+
181
+ ```python
182
+ await agent.send_form_widget(
183
+ conversation_id=message.conversation_id,
184
+ to=message.from_id,
185
+ widget_id="profile-form",
186
+ title="Complete profile",
187
+ submitLabel="Save",
188
+ fields=[
189
+ {"name": "email", "label": "Email", "type": "email", "required": True},
190
+ {"name": "notes", "label": "Notes", "type": "textarea"},
191
+ ],
192
+ )
193
+ ```
194
+
195
+ ### Progress
196
+
197
+ ```python
198
+ progress_id = await agent.send_progress_widget(
199
+ conversation_id=message.conversation_id,
200
+ to=message.from_id,
201
+ widget_id="import-progress",
202
+ title="Importing files",
203
+ value=10,
204
+ max=100,
205
+ cancellable=True,
206
+ )
207
+ ```
208
+
209
+ Update it:
210
+
211
+ ```python
212
+ await agent.send_widget_update(
213
+ conversation_id=message.conversation_id,
214
+ to=message.from_id,
215
+ ref=progress_id,
216
+ spec={"value": 65, "body": "Almost done"},
217
+ )
218
+ ```
219
+
220
+ ### File Picker
221
+
222
+ ```python
223
+ await agent.send_file_picker_widget(
224
+ conversation_id=message.conversation_id,
225
+ to=message.from_id,
226
+ widget_id="pick-files",
227
+ title="Choose files",
228
+ multiple=True,
229
+ accept=[".pdf", "image/*"],
230
+ max_files=5,
231
+ )
232
+ ```
233
+
234
+ ### Date And Time
235
+
236
+ ```python
237
+ await agent.send_datetime_widget(
238
+ conversation_id=message.conversation_id,
239
+ to=message.from_id,
240
+ widget_id="schedule",
241
+ title="Pick a meeting time",
242
+ mode="datetime",
243
+ min="2026-05-21T09:00:00",
244
+ )
245
+ ```
246
+
247
+ ## Files And Media
248
+
249
+ `send_file` and `send_media` encrypt bytes locally with AES-GCM, upload only
250
+ ciphertext to `/agent/blobs`, then send the attachment key and IV inside the
251
+ E2EE message payload.
252
+
253
+ ```python
254
+ await agent.send_file(
255
+ conversation_id=message.conversation_id,
256
+ to=message.from_id,
257
+ reply_to_message_id=message.id,
258
+ text="Here is the report.",
259
+ name="report.txt",
260
+ mime="text/plain",
261
+ data=b"private report",
262
+ )
263
+ ```
264
+
265
+ Media adds optional dimensions or duration:
266
+
267
+ ```python
268
+ await agent.send_media(
269
+ conversation_id=message.conversation_id,
270
+ to=message.from_id,
271
+ text="Preview attached.",
272
+ name="chart.png",
273
+ mime="image/png",
274
+ data=png_bytes,
275
+ width=1200,
276
+ height=800,
277
+ )
278
+ ```
279
+
280
+ ## Encryption
281
+
282
+ When `e2ee=True`, the SDK encrypts and decrypts messages locally before they
283
+ leave your machine. On connect, the client publishes the agent pre-key bundle,
284
+ establishes encrypted sessions for conversations, and keeps message contents
285
+ opaque to the relay.
286
+
287
+ Use `e2ee=False` only for local debugging or non-encrypted transports.
288
+
289
+ ## MCP
290
+
291
+ This package is the Python SDK for building agents. The MCP server is
292
+ distributed separately:
293
+
294
+ ```bash
295
+ npx -y @pacerelle/mcp-server
296
+ ```
@@ -0,0 +1,26 @@
1
+ # Releasing
2
+
3
+ The source tree keeps `AgentGatewayClient` pointed at `http://localhost:8080`
4
+ so local development works without extra configuration.
5
+
6
+ Release artifacts must be built with the public API URL injected:
7
+
8
+ ```bash
9
+ python scripts/build_release.py
10
+ uvx twine check dist/pacerelle-*
11
+ ```
12
+
13
+ The release script sets `PACERELLE_SDK_DEFAULT_BASE_URL=https://api.pacerelle.com`
14
+ for the Hatch build. The build hook restores the source file after the artifact
15
+ is created, so the working tree keeps the local default.
16
+
17
+ ## TestPyPI
18
+
19
+ ```bash
20
+ python scripts/upload_testpypi.py
21
+ ```
22
+
23
+ The current helper uploads the source distribution only. Wheels built directly
24
+ from the local WSL environment can use a raw `linux_aarch64` platform tag, which
25
+ TestPyPI rejects. Build publishable binary wheels with a manylinux-compatible
26
+ builder before uploading wheels.