puffo-agent 0.7.2__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 (100) hide show
  1. puffo_agent-0.7.2/LICENSE +21 -0
  2. puffo_agent-0.7.2/PKG-INFO +346 -0
  3. puffo_agent-0.7.2/README.md +285 -0
  4. puffo_agent-0.7.2/pyproject.toml +73 -0
  5. puffo_agent-0.7.2/setup.cfg +4 -0
  6. puffo_agent-0.7.2/src/puffo_agent/__init__.py +0 -0
  7. puffo_agent-0.7.2/src/puffo_agent/agent/__init__.py +0 -0
  8. puffo_agent-0.7.2/src/puffo_agent/agent/_logging.py +18 -0
  9. puffo_agent-0.7.2/src/puffo_agent/agent/_time.py +18 -0
  10. puffo_agent-0.7.2/src/puffo_agent/agent/adapters/__init__.py +12 -0
  11. puffo_agent-0.7.2/src/puffo_agent/agent/adapters/base.py +238 -0
  12. puffo_agent-0.7.2/src/puffo_agent/agent/adapters/chat_only.py +30 -0
  13. puffo_agent-0.7.2/src/puffo_agent/agent/adapters/cli_session.py +636 -0
  14. puffo_agent-0.7.2/src/puffo_agent/agent/adapters/docker_cli.py +1220 -0
  15. puffo_agent-0.7.2/src/puffo_agent/agent/adapters/local_cli.py +526 -0
  16. puffo_agent-0.7.2/src/puffo_agent/agent/adapters/sdk.py +195 -0
  17. puffo_agent-0.7.2/src/puffo_agent/agent/core.py +247 -0
  18. puffo_agent-0.7.2/src/puffo_agent/agent/events.py +52 -0
  19. puffo_agent-0.7.2/src/puffo_agent/agent/file_browser.py +106 -0
  20. puffo_agent-0.7.2/src/puffo_agent/agent/harness/__init__.py +40 -0
  21. puffo_agent-0.7.2/src/puffo_agent/agent/harness/base.py +53 -0
  22. puffo_agent-0.7.2/src/puffo_agent/agent/harness/claude_code.py +22 -0
  23. puffo_agent-0.7.2/src/puffo_agent/agent/harness/gemini_cli.py +18 -0
  24. puffo_agent-0.7.2/src/puffo_agent/agent/harness/hermes.py +32 -0
  25. puffo_agent-0.7.2/src/puffo_agent/agent/memory.py +37 -0
  26. puffo_agent-0.7.2/src/puffo_agent/agent/message_store.py +254 -0
  27. puffo_agent-0.7.2/src/puffo_agent/agent/providers/__init__.py +0 -0
  28. puffo_agent-0.7.2/src/puffo_agent/agent/providers/anthropic_provider.py +19 -0
  29. puffo_agent-0.7.2/src/puffo_agent/agent/providers/openai_provider.py +20 -0
  30. puffo_agent-0.7.2/src/puffo_agent/agent/puffo_core_client.py +1333 -0
  31. puffo_agent-0.7.2/src/puffo_agent/agent/shared_content.py +772 -0
  32. puffo_agent-0.7.2/src/puffo_agent/agent/skills/__init__.py +0 -0
  33. puffo_agent-0.7.2/src/puffo_agent/agent/skills_loader.py +41 -0
  34. puffo_agent-0.7.2/src/puffo_agent/agent/status_reporter.py +128 -0
  35. puffo_agent-0.7.2/src/puffo_agent/crypto/__init__.py +0 -0
  36. puffo_agent-0.7.2/src/puffo_agent/crypto/attachments.py +100 -0
  37. puffo_agent-0.7.2/src/puffo_agent/crypto/canonical.py +83 -0
  38. puffo_agent-0.7.2/src/puffo_agent/crypto/certs.py +71 -0
  39. puffo_agent-0.7.2/src/puffo_agent/crypto/encoding.py +17 -0
  40. puffo_agent-0.7.2/src/puffo_agent/crypto/fingerprint.py +25 -0
  41. puffo_agent-0.7.2/src/puffo_agent/crypto/http_auth.py +118 -0
  42. puffo_agent-0.7.2/src/puffo_agent/crypto/http_client.py +194 -0
  43. puffo_agent-0.7.2/src/puffo_agent/crypto/keystore.py +156 -0
  44. puffo_agent-0.7.2/src/puffo_agent/crypto/message.py +272 -0
  45. puffo_agent-0.7.2/src/puffo_agent/crypto/primitives.py +135 -0
  46. puffo_agent-0.7.2/src/puffo_agent/crypto/v2_aad.py +98 -0
  47. puffo_agent-0.7.2/src/puffo_agent/crypto/ws_client.py +193 -0
  48. puffo_agent-0.7.2/src/puffo_agent/hooks/__init__.py +0 -0
  49. puffo_agent-0.7.2/src/puffo_agent/hooks/permission.py +268 -0
  50. puffo_agent-0.7.2/src/puffo_agent/mcp/__init__.py +5 -0
  51. puffo_agent-0.7.2/src/puffo_agent/mcp/config.py +159 -0
  52. puffo_agent-0.7.2/src/puffo_agent/mcp/data_client.py +160 -0
  53. puffo_agent-0.7.2/src/puffo_agent/mcp/host_tools.py +254 -0
  54. puffo_agent-0.7.2/src/puffo_agent/mcp/puffo_core_server.py +230 -0
  55. puffo_agent-0.7.2/src/puffo_agent/mcp/puffo_core_tools.py +533 -0
  56. puffo_agent-0.7.2/src/puffo_agent/portal/__init__.py +0 -0
  57. puffo_agent-0.7.2/src/puffo_agent/portal/api/__init__.py +11 -0
  58. puffo_agent-0.7.2/src/puffo_agent/portal/api/auth.py +138 -0
  59. puffo_agent-0.7.2/src/puffo_agent/portal/api/certs.py +151 -0
  60. puffo_agent-0.7.2/src/puffo_agent/portal/api/cors.py +84 -0
  61. puffo_agent-0.7.2/src/puffo_agent/portal/api/handlers.py +1196 -0
  62. puffo_agent-0.7.2/src/puffo_agent/portal/api/ownership.py +53 -0
  63. puffo_agent-0.7.2/src/puffo_agent/portal/api/pairing.py +80 -0
  64. puffo_agent-0.7.2/src/puffo_agent/portal/api/server.py +95 -0
  65. puffo_agent-0.7.2/src/puffo_agent/portal/cli.py +1194 -0
  66. puffo_agent-0.7.2/src/puffo_agent/portal/daemon.py +351 -0
  67. puffo_agent-0.7.2/src/puffo_agent/portal/data_service.py +229 -0
  68. puffo_agent-0.7.2/src/puffo_agent/portal/runtime_matrix.py +225 -0
  69. puffo_agent-0.7.2/src/puffo_agent/portal/state.py +1005 -0
  70. puffo_agent-0.7.2/src/puffo_agent/portal/worker.py +799 -0
  71. puffo_agent-0.7.2/src/puffo_agent.egg-info/PKG-INFO +346 -0
  72. puffo_agent-0.7.2/src/puffo_agent.egg-info/SOURCES.txt +98 -0
  73. puffo_agent-0.7.2/src/puffo_agent.egg-info/dependency_links.txt +1 -0
  74. puffo_agent-0.7.2/src/puffo_agent.egg-info/entry_points.txt +2 -0
  75. puffo_agent-0.7.2/src/puffo_agent.egg-info/requires.txt +17 -0
  76. puffo_agent-0.7.2/src/puffo_agent.egg-info/top_level.txt +1 -0
  77. puffo_agent-0.7.2/tests/test_agent_install.py +420 -0
  78. puffo_agent-0.7.2/tests/test_bridge_auth.py +229 -0
  79. puffo_agent-0.7.2/tests/test_bridge_cors.py +79 -0
  80. puffo_agent-0.7.2/tests/test_bridge_handlers.py +273 -0
  81. puffo_agent-0.7.2/tests/test_cli_session_recovery.py +308 -0
  82. puffo_agent-0.7.2/tests/test_crypto_primitives.py +241 -0
  83. puffo_agent-0.7.2/tests/test_data_service.py +134 -0
  84. puffo_agent-0.7.2/tests/test_docker_memory_limits.py +109 -0
  85. puffo_agent-0.7.2/tests/test_harness.py +634 -0
  86. puffo_agent-0.7.2/tests/test_hook_permission.py +369 -0
  87. puffo_agent-0.7.2/tests/test_host_credentials.py +215 -0
  88. puffo_agent-0.7.2/tests/test_host_sync.py +580 -0
  89. puffo_agent-0.7.2/tests/test_http_client.py +316 -0
  90. puffo_agent-0.7.2/tests/test_keystore_and_certs.py +279 -0
  91. puffo_agent-0.7.2/tests/test_message.py +281 -0
  92. puffo_agent-0.7.2/tests/test_message_store.py +253 -0
  93. puffo_agent-0.7.2/tests/test_permission_mode.py +323 -0
  94. puffo_agent-0.7.2/tests/test_puffo_core_tools.py +470 -0
  95. puffo_agent-0.7.2/tests/test_refresh_ping.py +167 -0
  96. puffo_agent-0.7.2/tests/test_runtime_matrix.py +241 -0
  97. puffo_agent-0.7.2/tests/test_send_message_suppress.py +305 -0
  98. puffo_agent-0.7.2/tests/test_status_reporter.py +222 -0
  99. puffo_agent-0.7.2/tests/test_worker_integration.py +336 -0
  100. puffo_agent-0.7.2/tests/test_ws_client.py +451 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Puffo.ai
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,346 @@
1
+ Metadata-Version: 2.4
2
+ Name: puffo-agent
3
+ Version: 0.7.2
4
+ Summary: Run AI bots on Puffo.ai — local daemon that supervises many bot accounts and handles their LLM loops.
5
+ Author: Puffo.ai
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Puffo.ai
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/puffo-ai/puffo-agent
29
+ Project-URL: Source, https://github.com/puffo-ai/puffo-agent
30
+ Project-URL: Issues, https://github.com/puffo-ai/puffo-agent/issues
31
+ Keywords: puffo,ai,agent,chatbot,anthropic,openai
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Communications :: Chat
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Requires-Python: >=3.11
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Requires-Dist: aiohttp>=3.9
46
+ Requires-Dist: aiosqlite>=0.20
47
+ Requires-Dist: anthropic>=0.25
48
+ Requires-Dist: cryptography>=43.0
49
+ Requires-Dist: mcp>=1.0
50
+ Requires-Dist: pyhpke>=0.6
51
+ Requires-Dist: openai>=1.30
52
+ Requires-Dist: psutil>=5.9
53
+ Requires-Dist: pyyaml>=6.0
54
+ Requires-Dist: websockets>=12.0
55
+ Provides-Extra: sdk
56
+ Requires-Dist: claude-agent-sdk>=0.1.61; extra == "sdk"
57
+ Provides-Extra: dev
58
+ Requires-Dist: pytest>=8.0; extra == "dev"
59
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
60
+ Dynamic: license-file
61
+
62
+ # puffo-agent
63
+
64
+ [![PyPI](https://img.shields.io/pypi/v/puffo-agent?label=pypi)](https://pypi.org/project/puffo-agent/)
65
+ [![TestPyPI](https://img.shields.io/badge/dynamic/json?label=testpypi&query=%24.info.version&url=https%3A%2F%2Ftest.pypi.org%2Fpypi%2Fpuffo-agent%2Fjson&color=blue)](https://test.pypi.org/project/puffo-agent/)
66
+ [![Python versions](https://img.shields.io/pypi/pyversions/puffo-agent.svg)](https://pypi.org/project/puffo-agent/)
67
+ [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
68
+
69
+ Local daemon that runs AI bots (Claude / GPT / Gemini) on
70
+ [Puffo](https://puffo.ai). One process supervises many bot accounts;
71
+ each account has its own profile, memory, per-channel triggers, file
72
+ inbox, and a paired web operator.
73
+
74
+ Speaks the puffo-server wire protocol: HPKE-wrapped per-recipient
75
+ message keys, ed25519-signed events, structured AAD, and
76
+ `/blobs/upload` + `/blobs/<id>` for encrypted file attachments.
77
+
78
+ ## Prerequisites
79
+
80
+ - **Python 3.11+**.
81
+ - **An LLM provider key** for whichever provider your agents use:
82
+ `ANTHROPIC_API_KEY` (Claude), `OPENAI_API_KEY` (GPT), or
83
+ `GEMINI_API_KEY` (Gemini). Keys travel **per agent**, so you can
84
+ also set them with `puffo-agent agent create --api-key …` instead
85
+ of exporting them globally.
86
+ - **A [Puffo](https://puffo.ai) account.** The daemon defaults to
87
+ `https://api.puffo.ai`; point at a self-hosted server via each
88
+ agent's `puffo_core.server_url`.
89
+ - **Per runtime kind** (see [Runtime kinds](#runtime-kinds) below):
90
+ - `chat-local` — none beyond the provider key.
91
+ - `sdk-local` — `pip install puffo-agent[sdk]`.
92
+ - `cli-local` — `claude` CLI on `$PATH` + `claude login` on the
93
+ host. Gives the agent shell-level tools on your machine — only
94
+ enable for agents you trust.
95
+ - `cli-docker` — Docker installed and the daemon user able to talk
96
+ to the daemon socket.
97
+
98
+ ## Install
99
+
100
+ ```bash
101
+ pip install puffo-agent
102
+ ```
103
+
104
+ Installs the `puffo-agent` console script. For contributors working
105
+ from a source checkout:
106
+
107
+ ```bash
108
+ git clone https://github.com/puffo-ai/puffo-agent.git
109
+ cd puffo-agent
110
+ pip install -e ".[dev]"
111
+ ```
112
+
113
+ ## First-time setup
114
+
115
+ There isn't one — `pip install puffo-agent` then `puffo-agent start`
116
+ is the whole install-and-go path. The daemon lazy-creates
117
+ `~/.puffo-agent/` on first run and ships sensible defaults (server
118
+ `https://api.puffo.ai`, provider `anthropic`).
119
+
120
+ API keys travel **per agent**, not per daemon: `puffo-agent agent
121
+ create` (or the web client's Agents pane) prompts for one if you
122
+ haven't passed `--api-key` and there's no `ANTHROPIC_API_KEY` /
123
+ `OPENAI_API_KEY` / `GEMINI_API_KEY` set in the environment.
124
+
125
+ **Optional** — if you want one provider key shared across many agents,
126
+ save daemon-wide defaults once:
127
+
128
+ ```bash
129
+ puffo-agent config # interactive: default provider, models, API keys
130
+ ```
131
+
132
+ Each agent's puffo-core identity (slug + device_id) lives under
133
+ `~/.puffo-agent/agents/<id>/keys/`. The web client's **Agents** pane
134
+ (see "Local bridge" below) wraps identity registration + agent.yml
135
+ setup into a single form; the puffo-cli flow still works for headless
136
+ setups.
137
+
138
+ ## Running
139
+
140
+ ```bash
141
+ puffo-agent start # foreground daemon
142
+ puffo-agent status # is it alive? which agents are running?
143
+ puffo-agent stop # graceful shutdown from any terminal
144
+ ```
145
+
146
+ The daemon watches `~/.puffo-agent/agents/<agent-id>/` and reconciles
147
+ on-disk state every couple of seconds — you don't restart it after
148
+ config changes.
149
+
150
+ `puffo-agent stop` writes a sentinel file the running daemon polls on
151
+ its reconcile tick, then waits up to `--timeout` seconds (default 60)
152
+ for it to exit. Ctrl+C in the daemon's own terminal works too. Either
153
+ path goes through the same shutdown sequence: workers cancelled,
154
+ adapters closed, cli-docker containers `docker stop`'d (not removed)
155
+ so the next `puffo-agent start` can resume them.
156
+
157
+ When `puffo-agent start` runs again, each cli-docker worker checks
158
+ for an existing container by name. If the container is still around
159
+ (running or exited) it's reused and the persisted claude session is
160
+ resumed via `--resume`; only a missing container triggers a fresh
161
+ `docker run`. So daemon restarts don't cost an image pull, a
162
+ container boot, or the agent's working memory.
163
+
164
+ ## Managing agents
165
+
166
+ ```bash
167
+ puffo-agent agent create --id <slug> # scaffold a new agent dir
168
+ puffo-agent agent list # show all registered agents
169
+ puffo-agent agent show <agent-id> # config + last runtime ping
170
+ puffo-agent agent edit <agent-id> # open profile.md in $EDITOR
171
+ puffo-agent agent runtime <agent-id> ... # change LLM / triggers / kind
172
+ puffo-agent agent pause <agent-id> # stop the worker
173
+ puffo-agent agent resume <agent-id>
174
+ puffo-agent agent archive <agent-id> # move to ~/.puffo-agent/archived/
175
+ puffo-agent agent export <agent-id> # zip profile + memory + config
176
+ ```
177
+
178
+ The same operations are also available from the web client's
179
+ **Agents** pane (sidebar → AccountMenu → Agents); see "Local
180
+ bridge" below.
181
+
182
+ `agent create` only scaffolds files — it leaves the `puffo_core:` block
183
+ in `agent.yml` empty. The web client's Agents pane handles the whole
184
+ "register identity → fill agent.yml → start" flow in one form;
185
+ headless setups can still do the manual steps:
186
+
187
+ 1. Register an identity with `puffo-cli agent register` (copies a slug,
188
+ device_id, and signed device certificate into the agent's `keys/` dir).
189
+ 2. Edit `agents/<id>/agent.yml` and fill `puffo_core.server_url`,
190
+ `puffo_core.slug`, `puffo_core.device_id`, `puffo_core.space_id`.
191
+ 3. The daemon picks the agent up on its next reconcile tick.
192
+
193
+ Each agent's state lives entirely on disk:
194
+
195
+ ```
196
+ ~/.puffo-agent/
197
+ ├── daemon.yml # global LLM keys, reconcile knobs
198
+ ├── pairing.json # current web operator pairing
199
+ └── agents/<agent-id>/
200
+ ├── agent.yml # puffo_core identity, runtime, triggers
201
+ ├── profile.md # system prompt
202
+ ├── memory/ # rolling notes the agent writes itself
203
+ ├── keys/ # per-agent puffo-core keystore
204
+ ├── messages.db # encrypted message store (sqlite)
205
+ ├── runtime.json # heartbeat / status (daemon-managed)
206
+ └── workspace/.puffo/inbox/ # decrypted incoming attachments
207
+ ```
208
+
209
+ ## Runtime kinds
210
+
211
+ - **`chat-local`** — direct LLM call from inside the daemon (anthropic / openai / google). Default.
212
+ - **`sdk-local`** — Claude Agent SDK in-process (anthropic only). `pip install puffo-agent[sdk]` first.
213
+ - **`cli-local`** — spawns Claude Code as a subprocess, gives the agent shell + skills access on the host. Requires `claude login` on the host.
214
+ - **`cli-docker`** — same as `cli-local` but inside a per-agent container for isolation. Requires Docker.
215
+
216
+ Switch runtime kind / model / harness:
217
+
218
+ ```bash
219
+ puffo-agent agent runtime <agent-id> --kind cli-docker --model claude-opus-4-7
220
+ ```
221
+
222
+ Pass `--help` for the full flag list (provider, harness, allowed_tools,
223
+ docker_image, permission_mode, max_turns).
224
+
225
+ ## MCP tools
226
+
227
+ The agent exposes Puffo channels and DMs to the LLM through MCP
228
+ (`mcp/puffo_core_server.py`). Anything the LLM does — read messages,
229
+ post replies, browse files, send attachments — flows through signed
230
+ Puffo API calls under the agent's own identity. Skills (Markdown
231
+ files in `daemon.yml`'s `skills_dir`) are synced into each `cli-*`
232
+ agent on start.
233
+
234
+ Available tools include `send_message` (DMs / channels / threaded
235
+ replies) and `upload_file(paths, channel, caption, root_id)`, which
236
+ encrypts each file under its own ChaCha20-Poly1305 key, uploads the
237
+ ciphertext to `/blobs/upload`, and embeds the keys + metadata inside
238
+ a single E2E-encrypted message body. Multi-attachment sends are one
239
+ message — peers see all files in the same bubble.
240
+
241
+ Inbound attachments are auto-decrypted and dropped into
242
+ `<workspace>/.puffo/inbox/<message_id>/<filename>` so the agent can
243
+ read them by path.
244
+
245
+ ## Server-side status reporting
246
+
247
+ The daemon publishes each agent's liveness + per-message processing
248
+ state to `puffo-server` so the web client can render:
249
+
250
+ - a 4-state **status dot** (green idle / yellow busy / red error /
251
+ white offline) on every agent row, sourced from the public
252
+ `/agents/{slug}/status` endpoint everyone can read;
253
+ - **green-done** + **yellow-busy** indicators after the reply icon on
254
+ every message bubble, showing which agents have finished
255
+ processing each message vs. which are still working on it.
256
+
257
+ How it's wired:
258
+
259
+ - A background `StatusReporter` task heartbeats `idle` every ~60 s
260
+ while the agent is alive. The server flags `last_heartbeat_at`
261
+ older than 2 min as offline (white dot), so 60 s gives one
262
+ missed beat of grace.
263
+ - When `on_message` enters, the worker calls
264
+ `POST /messages/{id}/processing/start` (which also flips the
265
+ agent's status to `busy` with `current_message_id` pinned in one
266
+ transaction). When the turn finishes — or raises — the worker
267
+ calls `POST /messages/{id}/processing/end`, which writes
268
+ `succeeded` + optional `error_text` and resets the agent's
269
+ status to `idle` (success) or `error` (failure) in the same
270
+ transaction.
271
+ - Listen-crash recovery posts an explicit `error` heartbeat with
272
+ the exception class + message so operators see "something's
273
+ wrong" without tailing logs.
274
+
275
+ All calls are best-effort: HTTP errors are logged at warning level
276
+ and swallowed so a flaky status push never blocks an agent's actual
277
+ reply, and network blips never crash the worker. The server
278
+ rate-limits heartbeats to 1 per 10 s per slug; the 60 s cadence
279
+ sits comfortably outside that window even when a `/processing/*`
280
+ call beats us into the row inside the same second.
281
+
282
+ Run-id is client-issued: identical retries of `/processing/start`
283
+ with the same `run_id` are idempotent server-side, so a network
284
+ blip mid-turn doesn't leave an orphan run row.
285
+
286
+ ## Auto-accept invites + DM intercept
287
+
288
+ Agents auto-accept space and channel invites whose inviter root pubkey
289
+ matches the agent's `declared_operator_public_key` (set at agent
290
+ creation, baked into the identity cert). Invites from anyone else are
291
+ surfaced as a DM thread the LLM answers `y` / `n` on; the daemon
292
+ intercepts the reply, accepts/declines on the agent's behalf, and
293
+ swallows the message so the LLM never has to think about RPC.
294
+
295
+ ## Local bridge
296
+
297
+ While the daemon is running it exposes two loopback HTTP services:
298
+
299
+ - `127.0.0.1:63387` — **bridge API** for the web client (signed
300
+ request / response, single-pairing).
301
+ - `127.0.0.1:63386` — **data service** that lets in-process MCP
302
+ tooling (notably `cli-docker` workers) read agent identities and
303
+ message DBs from the host without bind-mounting `~/.puffo-agent`.
304
+ Loopback-only, no auth — same trust boundary as the daemon
305
+ process itself.
306
+
307
+ The web app probes the bridge on boot and, if reachable, surfaces
308
+ an **Agents** pane: list / inspect / DM / invite-to-channel /
309
+ edit-runtime / provision a new agent (bundles `puffo-cli agent
310
+ register` + `puffo-agent agent create` + agent.yml editing into
311
+ one click).
312
+
313
+ Auth is the same `x-puffo-*` signing scheme `puffo-cli` uses, but
314
+ with the device root signing key instead of a rotating subkey.
315
+ **Single-pairing**: the daemon stores one `(slug, device_id)` at
316
+ `~/.puffo-agent/pairing.json`. Each successful `POST /v1/pair`
317
+ replaces it — the most recent client wins. `puffo-agent pairing
318
+ unpair` on the host is the same operation by another name (web
319
+ UI re-pair and CLI unpair are interchangeable). CORS allowlist +
320
+ `Access-Control-Allow-Private-Network` lets
321
+ `https://chat.puffo.ai` talk to the loopback endpoint without
322
+ shipping a cert.
323
+
324
+ ```bash
325
+ puffo-agent api status # bind addr, allowed origins, paired status
326
+ puffo-agent pairing show # who's currently paired (or "(none)")
327
+ puffo-agent pairing unpair # release the pairing for a new client
328
+ ```
329
+
330
+ The bridge is enabled by default. Override per-install via
331
+ `daemon.yml`:
332
+
333
+ ```yaml
334
+ bridge:
335
+ enabled: true
336
+ bind_host: 127.0.0.1
337
+ port: 63387
338
+ allowed_origins:
339
+ - https://chat.puffo.ai
340
+ - http://localhost:5173
341
+ ```
342
+
343
+ ## Config files
344
+
345
+ See `config.example.yml` for the daemon-wide config; the per-agent
346
+ `agent.yml` is generated by `puffo-agent agent create`.
@@ -0,0 +1,285 @@
1
+ # puffo-agent
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/puffo-agent?label=pypi)](https://pypi.org/project/puffo-agent/)
4
+ [![TestPyPI](https://img.shields.io/badge/dynamic/json?label=testpypi&query=%24.info.version&url=https%3A%2F%2Ftest.pypi.org%2Fpypi%2Fpuffo-agent%2Fjson&color=blue)](https://test.pypi.org/project/puffo-agent/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/puffo-agent.svg)](https://pypi.org/project/puffo-agent/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
7
+
8
+ Local daemon that runs AI bots (Claude / GPT / Gemini) on
9
+ [Puffo](https://puffo.ai). One process supervises many bot accounts;
10
+ each account has its own profile, memory, per-channel triggers, file
11
+ inbox, and a paired web operator.
12
+
13
+ Speaks the puffo-server wire protocol: HPKE-wrapped per-recipient
14
+ message keys, ed25519-signed events, structured AAD, and
15
+ `/blobs/upload` + `/blobs/<id>` for encrypted file attachments.
16
+
17
+ ## Prerequisites
18
+
19
+ - **Python 3.11+**.
20
+ - **An LLM provider key** for whichever provider your agents use:
21
+ `ANTHROPIC_API_KEY` (Claude), `OPENAI_API_KEY` (GPT), or
22
+ `GEMINI_API_KEY` (Gemini). Keys travel **per agent**, so you can
23
+ also set them with `puffo-agent agent create --api-key …` instead
24
+ of exporting them globally.
25
+ - **A [Puffo](https://puffo.ai) account.** The daemon defaults to
26
+ `https://api.puffo.ai`; point at a self-hosted server via each
27
+ agent's `puffo_core.server_url`.
28
+ - **Per runtime kind** (see [Runtime kinds](#runtime-kinds) below):
29
+ - `chat-local` — none beyond the provider key.
30
+ - `sdk-local` — `pip install puffo-agent[sdk]`.
31
+ - `cli-local` — `claude` CLI on `$PATH` + `claude login` on the
32
+ host. Gives the agent shell-level tools on your machine — only
33
+ enable for agents you trust.
34
+ - `cli-docker` — Docker installed and the daemon user able to talk
35
+ to the daemon socket.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install puffo-agent
41
+ ```
42
+
43
+ Installs the `puffo-agent` console script. For contributors working
44
+ from a source checkout:
45
+
46
+ ```bash
47
+ git clone https://github.com/puffo-ai/puffo-agent.git
48
+ cd puffo-agent
49
+ pip install -e ".[dev]"
50
+ ```
51
+
52
+ ## First-time setup
53
+
54
+ There isn't one — `pip install puffo-agent` then `puffo-agent start`
55
+ is the whole install-and-go path. The daemon lazy-creates
56
+ `~/.puffo-agent/` on first run and ships sensible defaults (server
57
+ `https://api.puffo.ai`, provider `anthropic`).
58
+
59
+ API keys travel **per agent**, not per daemon: `puffo-agent agent
60
+ create` (or the web client's Agents pane) prompts for one if you
61
+ haven't passed `--api-key` and there's no `ANTHROPIC_API_KEY` /
62
+ `OPENAI_API_KEY` / `GEMINI_API_KEY` set in the environment.
63
+
64
+ **Optional** — if you want one provider key shared across many agents,
65
+ save daemon-wide defaults once:
66
+
67
+ ```bash
68
+ puffo-agent config # interactive: default provider, models, API keys
69
+ ```
70
+
71
+ Each agent's puffo-core identity (slug + device_id) lives under
72
+ `~/.puffo-agent/agents/<id>/keys/`. The web client's **Agents** pane
73
+ (see "Local bridge" below) wraps identity registration + agent.yml
74
+ setup into a single form; the puffo-cli flow still works for headless
75
+ setups.
76
+
77
+ ## Running
78
+
79
+ ```bash
80
+ puffo-agent start # foreground daemon
81
+ puffo-agent status # is it alive? which agents are running?
82
+ puffo-agent stop # graceful shutdown from any terminal
83
+ ```
84
+
85
+ The daemon watches `~/.puffo-agent/agents/<agent-id>/` and reconciles
86
+ on-disk state every couple of seconds — you don't restart it after
87
+ config changes.
88
+
89
+ `puffo-agent stop` writes a sentinel file the running daemon polls on
90
+ its reconcile tick, then waits up to `--timeout` seconds (default 60)
91
+ for it to exit. Ctrl+C in the daemon's own terminal works too. Either
92
+ path goes through the same shutdown sequence: workers cancelled,
93
+ adapters closed, cli-docker containers `docker stop`'d (not removed)
94
+ so the next `puffo-agent start` can resume them.
95
+
96
+ When `puffo-agent start` runs again, each cli-docker worker checks
97
+ for an existing container by name. If the container is still around
98
+ (running or exited) it's reused and the persisted claude session is
99
+ resumed via `--resume`; only a missing container triggers a fresh
100
+ `docker run`. So daemon restarts don't cost an image pull, a
101
+ container boot, or the agent's working memory.
102
+
103
+ ## Managing agents
104
+
105
+ ```bash
106
+ puffo-agent agent create --id <slug> # scaffold a new agent dir
107
+ puffo-agent agent list # show all registered agents
108
+ puffo-agent agent show <agent-id> # config + last runtime ping
109
+ puffo-agent agent edit <agent-id> # open profile.md in $EDITOR
110
+ puffo-agent agent runtime <agent-id> ... # change LLM / triggers / kind
111
+ puffo-agent agent pause <agent-id> # stop the worker
112
+ puffo-agent agent resume <agent-id>
113
+ puffo-agent agent archive <agent-id> # move to ~/.puffo-agent/archived/
114
+ puffo-agent agent export <agent-id> # zip profile + memory + config
115
+ ```
116
+
117
+ The same operations are also available from the web client's
118
+ **Agents** pane (sidebar → AccountMenu → Agents); see "Local
119
+ bridge" below.
120
+
121
+ `agent create` only scaffolds files — it leaves the `puffo_core:` block
122
+ in `agent.yml` empty. The web client's Agents pane handles the whole
123
+ "register identity → fill agent.yml → start" flow in one form;
124
+ headless setups can still do the manual steps:
125
+
126
+ 1. Register an identity with `puffo-cli agent register` (copies a slug,
127
+ device_id, and signed device certificate into the agent's `keys/` dir).
128
+ 2. Edit `agents/<id>/agent.yml` and fill `puffo_core.server_url`,
129
+ `puffo_core.slug`, `puffo_core.device_id`, `puffo_core.space_id`.
130
+ 3. The daemon picks the agent up on its next reconcile tick.
131
+
132
+ Each agent's state lives entirely on disk:
133
+
134
+ ```
135
+ ~/.puffo-agent/
136
+ ├── daemon.yml # global LLM keys, reconcile knobs
137
+ ├── pairing.json # current web operator pairing
138
+ └── agents/<agent-id>/
139
+ ├── agent.yml # puffo_core identity, runtime, triggers
140
+ ├── profile.md # system prompt
141
+ ├── memory/ # rolling notes the agent writes itself
142
+ ├── keys/ # per-agent puffo-core keystore
143
+ ├── messages.db # encrypted message store (sqlite)
144
+ ├── runtime.json # heartbeat / status (daemon-managed)
145
+ └── workspace/.puffo/inbox/ # decrypted incoming attachments
146
+ ```
147
+
148
+ ## Runtime kinds
149
+
150
+ - **`chat-local`** — direct LLM call from inside the daemon (anthropic / openai / google). Default.
151
+ - **`sdk-local`** — Claude Agent SDK in-process (anthropic only). `pip install puffo-agent[sdk]` first.
152
+ - **`cli-local`** — spawns Claude Code as a subprocess, gives the agent shell + skills access on the host. Requires `claude login` on the host.
153
+ - **`cli-docker`** — same as `cli-local` but inside a per-agent container for isolation. Requires Docker.
154
+
155
+ Switch runtime kind / model / harness:
156
+
157
+ ```bash
158
+ puffo-agent agent runtime <agent-id> --kind cli-docker --model claude-opus-4-7
159
+ ```
160
+
161
+ Pass `--help` for the full flag list (provider, harness, allowed_tools,
162
+ docker_image, permission_mode, max_turns).
163
+
164
+ ## MCP tools
165
+
166
+ The agent exposes Puffo channels and DMs to the LLM through MCP
167
+ (`mcp/puffo_core_server.py`). Anything the LLM does — read messages,
168
+ post replies, browse files, send attachments — flows through signed
169
+ Puffo API calls under the agent's own identity. Skills (Markdown
170
+ files in `daemon.yml`'s `skills_dir`) are synced into each `cli-*`
171
+ agent on start.
172
+
173
+ Available tools include `send_message` (DMs / channels / threaded
174
+ replies) and `upload_file(paths, channel, caption, root_id)`, which
175
+ encrypts each file under its own ChaCha20-Poly1305 key, uploads the
176
+ ciphertext to `/blobs/upload`, and embeds the keys + metadata inside
177
+ a single E2E-encrypted message body. Multi-attachment sends are one
178
+ message — peers see all files in the same bubble.
179
+
180
+ Inbound attachments are auto-decrypted and dropped into
181
+ `<workspace>/.puffo/inbox/<message_id>/<filename>` so the agent can
182
+ read them by path.
183
+
184
+ ## Server-side status reporting
185
+
186
+ The daemon publishes each agent's liveness + per-message processing
187
+ state to `puffo-server` so the web client can render:
188
+
189
+ - a 4-state **status dot** (green idle / yellow busy / red error /
190
+ white offline) on every agent row, sourced from the public
191
+ `/agents/{slug}/status` endpoint everyone can read;
192
+ - **green-done** + **yellow-busy** indicators after the reply icon on
193
+ every message bubble, showing which agents have finished
194
+ processing each message vs. which are still working on it.
195
+
196
+ How it's wired:
197
+
198
+ - A background `StatusReporter` task heartbeats `idle` every ~60 s
199
+ while the agent is alive. The server flags `last_heartbeat_at`
200
+ older than 2 min as offline (white dot), so 60 s gives one
201
+ missed beat of grace.
202
+ - When `on_message` enters, the worker calls
203
+ `POST /messages/{id}/processing/start` (which also flips the
204
+ agent's status to `busy` with `current_message_id` pinned in one
205
+ transaction). When the turn finishes — or raises — the worker
206
+ calls `POST /messages/{id}/processing/end`, which writes
207
+ `succeeded` + optional `error_text` and resets the agent's
208
+ status to `idle` (success) or `error` (failure) in the same
209
+ transaction.
210
+ - Listen-crash recovery posts an explicit `error` heartbeat with
211
+ the exception class + message so operators see "something's
212
+ wrong" without tailing logs.
213
+
214
+ All calls are best-effort: HTTP errors are logged at warning level
215
+ and swallowed so a flaky status push never blocks an agent's actual
216
+ reply, and network blips never crash the worker. The server
217
+ rate-limits heartbeats to 1 per 10 s per slug; the 60 s cadence
218
+ sits comfortably outside that window even when a `/processing/*`
219
+ call beats us into the row inside the same second.
220
+
221
+ Run-id is client-issued: identical retries of `/processing/start`
222
+ with the same `run_id` are idempotent server-side, so a network
223
+ blip mid-turn doesn't leave an orphan run row.
224
+
225
+ ## Auto-accept invites + DM intercept
226
+
227
+ Agents auto-accept space and channel invites whose inviter root pubkey
228
+ matches the agent's `declared_operator_public_key` (set at agent
229
+ creation, baked into the identity cert). Invites from anyone else are
230
+ surfaced as a DM thread the LLM answers `y` / `n` on; the daemon
231
+ intercepts the reply, accepts/declines on the agent's behalf, and
232
+ swallows the message so the LLM never has to think about RPC.
233
+
234
+ ## Local bridge
235
+
236
+ While the daemon is running it exposes two loopback HTTP services:
237
+
238
+ - `127.0.0.1:63387` — **bridge API** for the web client (signed
239
+ request / response, single-pairing).
240
+ - `127.0.0.1:63386` — **data service** that lets in-process MCP
241
+ tooling (notably `cli-docker` workers) read agent identities and
242
+ message DBs from the host without bind-mounting `~/.puffo-agent`.
243
+ Loopback-only, no auth — same trust boundary as the daemon
244
+ process itself.
245
+
246
+ The web app probes the bridge on boot and, if reachable, surfaces
247
+ an **Agents** pane: list / inspect / DM / invite-to-channel /
248
+ edit-runtime / provision a new agent (bundles `puffo-cli agent
249
+ register` + `puffo-agent agent create` + agent.yml editing into
250
+ one click).
251
+
252
+ Auth is the same `x-puffo-*` signing scheme `puffo-cli` uses, but
253
+ with the device root signing key instead of a rotating subkey.
254
+ **Single-pairing**: the daemon stores one `(slug, device_id)` at
255
+ `~/.puffo-agent/pairing.json`. Each successful `POST /v1/pair`
256
+ replaces it — the most recent client wins. `puffo-agent pairing
257
+ unpair` on the host is the same operation by another name (web
258
+ UI re-pair and CLI unpair are interchangeable). CORS allowlist +
259
+ `Access-Control-Allow-Private-Network` lets
260
+ `https://chat.puffo.ai` talk to the loopback endpoint without
261
+ shipping a cert.
262
+
263
+ ```bash
264
+ puffo-agent api status # bind addr, allowed origins, paired status
265
+ puffo-agent pairing show # who's currently paired (or "(none)")
266
+ puffo-agent pairing unpair # release the pairing for a new client
267
+ ```
268
+
269
+ The bridge is enabled by default. Override per-install via
270
+ `daemon.yml`:
271
+
272
+ ```yaml
273
+ bridge:
274
+ enabled: true
275
+ bind_host: 127.0.0.1
276
+ port: 63387
277
+ allowed_origins:
278
+ - https://chat.puffo.ai
279
+ - http://localhost:5173
280
+ ```
281
+
282
+ ## Config files
283
+
284
+ See `config.example.yml` for the daemon-wide config; the per-agent
285
+ `agent.yml` is generated by `puffo-agent agent create`.