This release is a pre-release and may not be stable for production use.
Pacerelle Python SDK
Python agent client for Pacerelle encrypted local agent relays.
For bounded, revocable permissions on local operations, see runtime permissions.
Use this SDK to connect a local Python process to Pacerelle, receive messages, reply to conversations, drive widgets, and return encrypted files or media.
Restarting an agent and replying to multiple sessions
Keep the same agent ID and store_root across restarts. With e2ee=True, the
SQLite store persists the Signal identity and prekeys, conversation archive keys,
decoded requests awaiting acceptance, accepted message IDs and each source's
device. Existing opaque Signal snapshots migrate when next saved. The Python
wrapper is specific to Python; do not copy it into another SDK's store.
Use reply_to_message_id=message.id on delayed replies. The SDK retains the
source device, so a later request from a second browser does not redirect the
first reply. Reading a duplicate accepted message acknowledges it again without
calling the handler. A request interrupted before acceptance remains available
on relay replay after reconnecting, including a request already decrypted.
The handler's successful return means the runtime has accepted responsibility.
Persist the task before returning if work continues in the background. An
acknowledgement does not prove that an action finished. The handler can run again
after a failure or a crash before its acceptance commit: use the message ID as an
idempotency key for effects outside the SDK. send_message reports submission to
the socket; it does not wait for relay acknowledgement or guarantee exactly-once
execution. Supervise connect() and reconnect after a network or handler error.
Clients advertising history-v1 can supply a conversation archive key inside an
encrypted Signal delivery. Python unwraps it before the handler and attaches an
AES-256-GCM archive record to subsequent replies. The relay receives ciphertext;
the archive key stays with endpoints. The archive binds conversation, message,
sender, key ID and epoch. This covers messages produced with the protocol; it
does not recreate old missing plaintext or lost keys. Automatic key rotation is not
provided by this Python client.
Use one running agent process per store directory. The SQLite file contains private keys and pending plaintext; protect the directory and its backups with the operating system. The current store does not encrypt the file or coordinate multiple writers at the agent level. Receipts are retained with the store and grow with accepted work. Losing or manually deleting the store loses its replay protection and Signal state. A peer identity notification or a failed decrypt does not reset unrelated sessions.
To compare the program's published Signal key through its local terminal:
client.publish_prekey_bundle()
print(client.get_verification_code())
Compare the full code with the agent verification dialog in Pacerelle. The getter
does not create new prekeys, and connect() reuses this publication. A code sent
through the conversation is not an independent comparison.
pip install --pre pacerelle
Alpha release: APIs may change before the first stable release. Production wheels bundle the native Signal runtime for the target platform.
Requirements
- Python 3.12 for the current alpha wheels.
- A supported platform wheel: Windows x64, Linux x64, Linux ARM64, or macOS ARM64.
Python 3.11 support is planned, but the current alpha release is tested and published for CPython 3.12 only.
Before You Run
Create an agent in Pacerelle. The confirmation panel shows both
Identifiant de l'agent and Jeton d'authentification. Use
Copier la configuration .env to copy the required variables.
export PACERELLE_AGENT_ID="agent-id"
export PACERELLE_AGENT_TOKEN="agent-token"
On Windows PowerShell:
$env:PACERELLE_AGENT_ID = "agent-id"
$env:PACERELLE_AGENT_TOKEN = "agent-token"
Published packages connect to the Pacerelle API by default. Local source builds
default to http://localhost:8080 for development.
Agent Connect
Third-party Python applications can install an agent only after the user approves the request in Pacerelle. Keep the PKCE verifier, state, installation token, and runtime token on the application server.
from pacerelle import (
begin_agent_connect,
exchange_agent_connect_code,
request_agent_runtime_token,
)
pending = begin_agent_connect(
connect_key=user_supplied_connect_key,
client_id=pacerelle_client_id,
redirect_uri="https://your-app.example/pacerelle/callback",
agent_name="Research assistant",
)
# Redirect the user to pending.authorization_url and verify pending.state.
installation = exchange_agent_connect_code(
code=callback_code,
code_verifier=pending.code_verifier,
client_id=pacerelle_client_id,
redirect_uri="https://your-app.example/pacerelle/callback",
)
runtime = request_agent_runtime_token(
installation_token=installation.installation_token,
)
Minimal Echo Agent
import asyncio
import os
from pacerelle import AgentGatewayClient
client = AgentGatewayClient(
token=os.environ["PACERELLE_AGENT_TOKEN"],
agent_id=os.environ["PACERELLE_AGENT_ID"],
e2ee=True,
)
async def handle(message, agent):
await agent.send_message(
conversation_id=message.conversation_id,
to=message.from_id,
reply_to_message_id=message.id,
text=f"Received: {message.text}",
)
client.on_message(handle)
asyncio.run(client.connect())
Incoming Messages
The handler receives an AgentMessage:
async def handle(message, agent):
print(message.id)
print(message.conversation_id)
print(message.from_id)
print(message.text)
print(message.attachments)
print(message.widget_response)
Use message.from_id as the to value when replying to the user.
Sending Messages And Replies
Send a normal message:
await agent.send_message(
conversation_id=message.conversation_id,
to=message.from_id,
text="I can help with that.",
)
Reply to a specific user message:
await agent.send_message(
conversation_id=message.conversation_id,
to=message.from_id,
reply_to_message_id=message.id,
text="Replying to your last message.",
)
Running Your Own Agent Logic
async def handle(message, agent):
result = await run_my_agent(message.text)
await agent.send_message(
conversation_id=message.conversation_id,
to=message.from_id,
reply_to_message_id=message.id,
text=result,
)
Widgets
Widgets are sent as encrypted conversation messages. Each method returns the
widget id. User answers arrive later as message.widget_response.
Confirm
await agent.send_confirm_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="confirm-delete",
title="Delete file?",
body="This cannot be undone.",
danger=True,
labels={"yes": "Delete", "no": "Cancel"},
)
Handle the answer:
if message.widget_response and message.widget_response.ref == "confirm-delete":
if message.widget_response.cancelled:
return
if message.widget_response.value is True:
await agent.send_message(
conversation_id=message.conversation_id,
to=message.from_id,
text="Confirmed.",
)
Choice
await agent.send_choice_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="choose-format",
title="Choose a format",
options=[
{"id": "pdf", "label": "PDF"},
{"id": "csv", "label": "CSV"},
],
multi=False,
)
Permission
await agent.send_permission_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="permission-files",
title="Allow file access?",
body="The agent needs access to selected files.",
scopes=["once", "session"],
)
Form
await agent.send_form_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="profile-form",
title="Complete profile",
submitLabel="Save",
fields=[
{"name": "email", "label": "Email", "type": "email", "required": True},
{"name": "notes", "label": "Notes", "type": "textarea"},
],
)
Progress
progress_id = await agent.send_progress_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="import-progress",
title="Importing files",
value=10,
max=100,
cancellable=True,
)
Update it:
await agent.send_widget_update(
conversation_id=message.conversation_id,
to=message.from_id,
ref=progress_id,
spec={"value": 65, "body": "Almost done"},
)
File Picker
await agent.send_file_picker_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="pick-files",
title="Choose files",
multiple=True,
accept=[".pdf", "image/*"],
max_files=5,
)
Date And Time
await agent.send_datetime_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="schedule",
title="Pick a meeting time",
mode="datetime",
min="2026-05-21T09:00:00",
)
Files And Media
send_file and send_media encrypt bytes locally with AES-GCM, upload only
ciphertext to /agent/blobs, then send the attachment key, IV, file name, type
and dimensions inside the E2EE message payload. The relay only sees a generic
name and the size.
Download and decrypt a file received from a user:
async def handle(message, agent):
for attachment in message.attachments or []:
data = await agent.download_attachment(attachment)
print(attachment.name, attachment.mime, len(data))
await agent.send_file(
conversation_id=message.conversation_id,
to=message.from_id,
reply_to_message_id=message.id,
text="Here is the report.",
name="report.txt",
mime="text/plain",
data=b"private report",
)
Media adds optional dimensions or duration:
await agent.send_media(
conversation_id=message.conversation_id,
to=message.from_id,
text="Preview attached.",
name="chart.png",
mime="image/png",
data=png_bytes,
width=1200,
height=800,
)
Encryption
When e2ee=True, the SDK encrypts and decrypts messages locally before they
leave your machine. On connect, the client publishes the agent pre-key bundle,
establishes encrypted sessions for conversations, and keeps message contents
opaque to the relay.
Use e2ee=False only for local debugging or non-encrypted transports.
Groups
Group messages are encrypted once with a group key that human members distribute
to each member over Signal. The client fetches it from /agent/group-keys,
stores it with the rest of its state, and uses it for group targets
(group:<conversation_id>). reply() answers in the group automatically:
async def handle(message, agent):
if message.text.strip() == "!status":
await agent.reply(message, "All green")
Collective votes use response_mode="collective" on send_choice_widget or
send_confirm_widget.
MCP
This package is the Python SDK for building agents. The MCP server is distributed separately:
npx -y @pacerelle/mcp-server
Release files for pacerelle 0.1.0a7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pacerelle-0.1.0a7.tar.gz | 2.5 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| pacerelle-0.1.0a7-cp312-cp312-win_amd64.whl | CPython 3.12 | CPython 3.12 | Windows x86-64 | Details |
| pacerelle-0.1.0a7-cp312-cp312-manylinux_2_34_x86_64.whl | CPython 3.12 | CPython 3.12 | Linux glibc 2.34+ x86-64 | Details |
| pacerelle-0.1.0a7-cp312-cp312-manylinux_2_34_aarch64.whl | CPython 3.12 | CPython 3.12 | Linux glibc 2.34+ ARM64 | Details |
| pacerelle-0.1.0a7-cp312-cp312-macosx_14_0_arm64.whl | CPython 3.12 | CPython 3.12 | macOS 14.0+ ARM64 | Details |
Total release size: 5.6 MB
Release files / pacerelle-0.1.0a7.tar.gz
| Download URL | pacerelle-0.1.0a7.tar.gz |
|---|---|
| Size | 2.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1951c6e89291a3b51a2c795ea2ee385a7fa0d420d58723246b6c824467369c7b
|
|
BLAKE2b-256 checksum How to use checksums |
30b1d10fcd4210dc35cd0871d61161123d7d2bd7caab50825d8a4dada72d9ea6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pacerelle-0.1.0a7-cp312-cp312-win_amd64.whl
| Download URL | pacerelle-0.1.0a7-cp312-cp312-win_amd64.whl |
|---|---|
| Size | 787.0 kB |
| Tags | CPython 3.12 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
36735eaf9630eecce06d674928352b54fda774d0277f9d2f793a4fcc21036eca
|
|
BLAKE2b-256 checksum How to use checksums |
8bb897ccf3f5fe33e880a18619ebd0b8f399044de5d7e0f0ec76ca2603a471e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pacerelle-0.1.0a7-cp312-cp312-manylinux_2_34_x86_64.whl
| Download URL | pacerelle-0.1.0a7-cp312-cp312-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 925.4 kB |
| Tags | CPython 3.12 Linux glibc 2.34+ x86-64 |
|
SHA-256 checksum How to use checksums |
44693b4c3f393e0053157c761270f405401374115b0caf1c25ed5e32ef880e64
|
|
BLAKE2b-256 checksum How to use checksums |
b3bec662bfd70a69740ec854ef438bde21d8387b4c4325fda3d88d1257318af1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pacerelle-0.1.0a7-cp312-cp312-manylinux_2_34_aarch64.whl
| Download URL | pacerelle-0.1.0a7-cp312-cp312-manylinux_2_34_aarch64.whl |
|---|---|
| Size | 752.8 kB |
| Tags | CPython 3.12 Linux glibc 2.34+ ARM64 |
|
SHA-256 checksum How to use checksums |
41d9ea49e165351f901149bf3086797f717cd1bf915b29561011a40fba6f0351
|
|
BLAKE2b-256 checksum How to use checksums |
16c0458677453ec771e48999d78d060ccb8e610823576946be3de3dfc5488bb5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pacerelle-0.1.0a7-cp312-cp312-macosx_14_0_arm64.whl
| Download URL | pacerelle-0.1.0a7-cp312-cp312-macosx_14_0_arm64.whl |
|---|---|
| Size | 632.6 kB |
| Tags | CPython 3.12 macOS 14.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
9b3b3d32de18407270576dfb97333a3129f426e6b8689f0c5d68eedeaac976dc
|
|
BLAKE2b-256 checksum How to use checksums |
2b45af99f1a04bdf22ce83abc547f5a75d26fc09e8b3d361c637167aebf4ed12
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|