discord-os 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. discord_os-0.3.0/LICENSE +21 -0
  2. discord_os-0.3.0/PKG-INFO +291 -0
  3. discord_os-0.3.0/README.md +264 -0
  4. discord_os-0.3.0/pyproject.toml +45 -0
  5. discord_os-0.3.0/setup.cfg +4 -0
  6. discord_os-0.3.0/src/agent_discord/__init__.py +12 -0
  7. discord_os-0.3.0/src/agent_discord/__main__.py +3 -0
  8. discord_os-0.3.0/src/agent_discord/bootstrap.py +79 -0
  9. discord_os-0.3.0/src/agent_discord/cli.py +1393 -0
  10. discord_os-0.3.0/src/agent_discord/config.py +331 -0
  11. discord_os-0.3.0/src/agent_discord/contracts.py +456 -0
  12. discord_os-0.3.0/src/agent_discord/discord/__init__.py +14 -0
  13. discord_os-0.3.0/src/agent_discord/discord/chunking.py +57 -0
  14. discord_os-0.3.0/src/agent_discord/discord/errors.py +27 -0
  15. discord_os-0.3.0/src/agent_discord/discord/facade.py +256 -0
  16. discord_os-0.3.0/src/agent_discord/discord/gateway.py +65 -0
  17. discord_os-0.3.0/src/agent_discord/discord/interactions.py +245 -0
  18. discord_os-0.3.0/src/agent_discord/discord/invite.py +25 -0
  19. discord_os-0.3.0/src/agent_discord/discord/object_store.py +255 -0
  20. discord_os-0.3.0/src/agent_discord/discord/providers/__init__.py +74 -0
  21. discord_os-0.3.0/src/agent_discord/discord/providers/base.py +394 -0
  22. discord_os-0.3.0/src/agent_discord/discord/providers/braindao.py +163 -0
  23. discord_os-0.3.0/src/agent_discord/discord/providers/fake.py +388 -0
  24. discord_os-0.3.0/src/agent_discord/discord/providers/rest.py +154 -0
  25. discord_os-0.3.0/src/agent_discord/discord/providers/saseq.py +616 -0
  26. discord_os-0.3.0/src/agent_discord/discord/realtime.py +157 -0
  27. discord_os-0.3.0/src/agent_discord/discord/rest.py +428 -0
  28. discord_os-0.3.0/src/agent_discord/discord/ws.py +167 -0
  29. discord_os-0.3.0/src/agent_discord/host/__init__.py +18 -0
  30. discord_os-0.3.0/src/agent_discord/host/actions.py +126 -0
  31. discord_os-0.3.0/src/agent_discord/host/install.py +212 -0
  32. discord_os-0.3.0/src/agent_discord/host/panel.py +128 -0
  33. discord_os-0.3.0/src/agent_discord/host/power.py +35 -0
  34. discord_os-0.3.0/src/agent_discord/host/service.py +146 -0
  35. discord_os-0.3.0/src/agent_discord/host/verbs.py +110 -0
  36. discord_os-0.3.0/src/agent_discord/keys/__init__.py +10 -0
  37. discord_os-0.3.0/src/agent_discord/keys/connect.py +278 -0
  38. discord_os-0.3.0/src/agent_discord/keys/vault.py +171 -0
  39. discord_os-0.3.0/src/agent_discord/marionette/__init__.py +17 -0
  40. discord_os-0.3.0/src/agent_discord/marionette/backend.py +438 -0
  41. discord_os-0.3.0/src/agent_discord/marionette/fake.py +150 -0
  42. discord_os-0.3.0/src/agent_discord/marionette/transport.py +77 -0
  43. discord_os-0.3.0/src/agent_discord/orchestration/__init__.py +14 -0
  44. discord_os-0.3.0/src/agent_discord/orchestration/cards.py +120 -0
  45. discord_os-0.3.0/src/agent_discord/orchestration/listen.py +440 -0
  46. discord_os-0.3.0/src/agent_discord/orchestration/orchestrator.py +497 -0
  47. discord_os-0.3.0/src/agent_discord/orchestration/receipts.py +72 -0
  48. discord_os-0.3.0/src/agent_discord/persistence/__init__.py +6 -0
  49. discord_os-0.3.0/src/agent_discord/persistence/research.py +389 -0
  50. discord_os-0.3.0/src/agent_discord/persistence/sqlite.py +859 -0
  51. discord_os-0.3.0/src/agent_discord/puppetmaster/__init__.py +23 -0
  52. discord_os-0.3.0/src/agent_discord/puppetmaster/agentic.py +207 -0
  53. discord_os-0.3.0/src/agent_discord/puppetmaster/backend.py +333 -0
  54. discord_os-0.3.0/src/agent_discord/puppetmaster/fake.py +144 -0
  55. discord_os-0.3.0/src/agent_discord/puppetmaster/models.py +22 -0
  56. discord_os-0.3.0/src/agent_discord/redaction.py +54 -0
  57. discord_os-0.3.0/src/discord_os.egg-info/PKG-INFO +291 -0
  58. discord_os-0.3.0/src/discord_os.egg-info/SOURCES.txt +78 -0
  59. discord_os-0.3.0/src/discord_os.egg-info/dependency_links.txt +1 -0
  60. discord_os-0.3.0/src/discord_os.egg-info/entry_points.txt +3 -0
  61. discord_os-0.3.0/src/discord_os.egg-info/requires.txt +6 -0
  62. discord_os-0.3.0/src/discord_os.egg-info/top_level.txt +1 -0
  63. discord_os-0.3.0/tests/test_bootstrap_config.py +154 -0
  64. discord_os-0.3.0/tests/test_chunking_and_facade.py +151 -0
  65. discord_os-0.3.0/tests/test_cli.py +66 -0
  66. discord_os-0.3.0/tests/test_connect_os.py +662 -0
  67. discord_os-0.3.0/tests/test_discord_os.py +620 -0
  68. discord_os-0.3.0/tests/test_discord_rest.py +235 -0
  69. discord_os-0.3.0/tests/test_host_actions.py +175 -0
  70. discord_os-0.3.0/tests/test_host_panel.py +204 -0
  71. discord_os-0.3.0/tests/test_host_power.py +210 -0
  72. discord_os-0.3.0/tests/test_interactions.py +205 -0
  73. discord_os-0.3.0/tests/test_invite.py +34 -0
  74. discord_os-0.3.0/tests/test_marionette_backend.py +118 -0
  75. discord_os-0.3.0/tests/test_mcp_transport.py +207 -0
  76. discord_os-0.3.0/tests/test_model_pin.py +31 -0
  77. discord_os-0.3.0/tests/test_orchestration.py +151 -0
  78. discord_os-0.3.0/tests/test_puppetmaster_cli_backend.py +147 -0
  79. discord_os-0.3.0/tests/test_research_memory.py +235 -0
  80. discord_os-0.3.0/tests/test_sqlite_memory.py +145 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Discord OS contributors
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,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: discord-os
3
+ Version: 0.3.0
4
+ Summary: Discord OS: Discord is the harness UI for local agent work. Artifacts are Discord objects (snowflake IDs).
5
+ Author: Discord OS contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/professorpalmer/agent-discord
8
+ Project-URL: Attribution-SaseQ, https://github.com/SaseQ/discord-mcp
9
+ Project-URL: Attribution-BrainDAO, https://github.com/BrainDAO/mcp-discord
10
+ Keywords: discord,discord-os,mcp,puppetmaster,agent,cli
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Communications :: Chat
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Provides-Extra: interactions
25
+ Requires-Dist: PyNaCl>=1.5; extra == "interactions"
26
+ Dynamic: license-file
27
+
28
+ # Discord OS
29
+
30
+ Discord is the screen. This process is the computer. Your phone is the remote.
31
+
32
+ This is **your** bot on **your** machine. There is no hosted fleet to invite. Leave the host running; turn work on and off from Discord.
33
+
34
+ The GitHub repo is [`professorpalmer/agent-discord`](https://github.com/professorpalmer/agent-discord). The `agent-discord` command still works. Env vars and the `.agent-discord` workspace directory stay as they are.
35
+
36
+ ## 1. Make a Discord bot
37
+
38
+ Do this once in a browser.
39
+
40
+ 1. Open the [Discord Developer Portal](https://discord.com/developers/applications) and sign in.
41
+ 2. **New Application**. Name it whatever you want (this is the bot people will see).
42
+ 3. Left sidebar → **Bot**.
43
+ 4. **Reset Token** / **Copy**. The token looks like `xxx.yyy.zzz`. That is `DISCORD_BOT_TOKEN`.
44
+ - Do **not** use Application ID.
45
+ - Do **not** use the OAuth client secret.
46
+ 5. On the same Bot page, enable **Message Content Intent**. Save changes.
47
+ 6. Left sidebar → **General Information**. Copy **Application ID** (digits only). That is `DISCORD_APPLICATION_ID`.
48
+ 7. In Discord, create or pick a private staff channel. Copy its channel ID (Developer Mode → right-click the channel → Copy Channel ID).
49
+
50
+ ## 2. Install once on the machine that will do the work
51
+
52
+ Python 3.11+. From a clone of `dev` today (`pip install discord-os` after PyPI):
53
+
54
+ ```bash
55
+ python3 -m venv .venv
56
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
57
+ pip install -e .
58
+ pip install puppetmaster-ai # compute kernel; not bundled
59
+ export OPENROUTER_API_KEY=... # never commit
60
+ discord-os bootstrap
61
+ ```
62
+
63
+ Put these in `.env` (or write the bot token to `~/.pmharness/.discord_token`, mode 0600):
64
+
65
+ ```bash
66
+ DISCORD_BOT_TOKEN=xxx.yyy.zzz
67
+ DISCORD_APPLICATION_ID=123456789012345678
68
+ ```
69
+
70
+ Then one command:
71
+
72
+ ```bash
73
+ discord-os setup --channel-id YOUR_CHANNEL_ID
74
+ ```
75
+
76
+ That invites the bot (open the printed URL), installs a login helper so it comes back after reboot, and posts a HOST card in the channel with **On** and **Off** buttons. You do not type commands in Discord after this.
77
+
78
+ ## 3. Use it from Discord
79
+
80
+ | In Discord | What happens |
81
+ |---|---|
82
+ | **On** | Starts work on the host. Type a normal sentence as a task. |
83
+ | **Off** | Stops work. The helper stays so On still works from your phone. |
84
+ | a normal sentence | A task, only while On. |
85
+
86
+ If Discord logs the bot out, work stops. The login helper starts again idle.
87
+
88
+ No Docker. No slash commands. No public URL. No `/on` to remember.
89
+
90
+ Default I/O is Discord REST. SaseQ/BrainDAO MCP is optional if you already run those servers.
91
+
92
+ ## Thesis
93
+
94
+ Discord already is the phone UI, the ACL (a channel you control), the notification bus, and the identity layer. Discord OS treats **artifacts as Discord objects**:
95
+
96
+ ```text
97
+ object bytes
98
+ → attachment + caption on a channel message
99
+ → Discord CDN (ephemeral signed URL, ~24h)
100
+ → durable key = channel_id / message_id / attachment_id (+ sha256)
101
+ → retrieve later by re-fetching the message (fresh URL), then downloading
102
+ ```
103
+
104
+ Never persist a CDN URL as the durable key. Official retrieve is Get Channel Message (or `POST /attachments/refresh-urls`). Jump links (`https://discord.com/channels/{guild|@me}/{channel}/{message}`) are what we print for humans.
105
+
106
+ ## Prior art (steal economics, not product)
107
+
108
+ Two piles exist. We are neither.
109
+
110
+ | Pile | Examples | What they do | What we take | What we refuse |
111
+ |------|----------|--------------|--------------|----------------|
112
+ | Discord-as-disk | DiscordFS, forscht/ddrive, KITdt/discord-drive, missuo/discord-image | Chunk files into channels as free S3 / WebDav | Snowflake ID pointer; refresh-on-get | Unlimited chunked S3, public CDN, WebDav, 4TB multipart |
113
+ | Discord-as-agent-relay | Discode, Agent4Discord, Agentboard, cursor-mobile-bridge, claudecode-discord | Channel=workspace, thread=run, phone UI; artifacts stay on local disk/tmux | Channel as workspace / ACL; phone-native receipts | Relaying progress while leaving blobs on disk only |
114
+
115
+ Discord has said: if you host files on Discord, find a more suitable service. Honest S3 replacement here means **agent artifacts under Discord size limits in your own staff channel**. It does not mean a public CDN.
116
+
117
+ Default live I/O is **Discord REST** with the Bot token (`DISCORD_BOT_TOKEN` or `~/.pmharness/.discord_token`). That value is the Bot token (`xxx.yyy.zzz`) from the Bot tab — not the Application client ID and not the OAuth client secret. Optional SaseQ/BrainDAO MCP adapters try file-tool names first; if the catalog has no file tool they fall back to the same REST path. We still refuse to base64-dump files into `send_message`. The fake provider is the hermetic proof of the protocol. Foreground `listen` does **not** open a Discord Gateway. `host` / `setup` opens a Gateway **only** so On/Off buttons work — no public URL. Do not run that beside another bot process that already owns the Gateway.
118
+
119
+ ## Honest limits
120
+
121
+ - Default object cap is **10 MiB** (`DISCORD_MAX_OBJECT_BYTES=10485760`). Discord free is roughly 10–25MB; Nitro is higher. Configurable, not unlimited.
122
+ - Discord ToS: conversation artifacts in **your** server, not a public CDN or anonymous disk.
123
+ - Not a compute host. Default compute is `AGENT_DISCORD_COMPUTE=auto`: Puppetmaster **agentic** (`openrouter/auto`) when an OpenRouter key is on the host or in the workspace vault; otherwise the Cursor pin (`cursor/grok-4-5` / adapter `grok-4.5`). **No silent model fallback.**
124
+ - No DiscordFS-style multipart chunking. Oversize artifacts become an `overflow` pointer + local stash, not multipart CDN objects.
125
+ - `/connect <secret>` and `/open` are message-prefix verbs by default, not Discord slash Interactions. Discord still sees shred payloads once before delete.
126
+ - `listen` ignores channel history older than a durable per-channel SQLite watermark (first listen: now minus 15s, same slack as process start; later processes resume from the stored high-water, including skipped cards/connects/opens) so a seeded staff channel is not dispatched as an implement job.
127
+
128
+ ## What a run does
129
+
130
+ 1. **Connect** (optional): `/connect` on the listen host inherits `OPENROUTER_API_KEY`, shreds a pasted secret after delete, or mints a pairing ticket for `discord-os connect --ticket`.
131
+ 2. **Intake** a natural-language task from `run` or the host loop (staff channel / phone). Cards, receipts, and object-store captions are skipped. On/Off buttons, `/connect`, and `/open` are intercepted before task dispatch. Work is accepted only while On. Discord is the remote; this process opens Terminal, the file manager, or an allowlisted browser on the host.
132
+ 3. **Snapshot** scoped context from SQLite memory + channel bindings.
133
+ 4. **Dispatch** to Puppetmaster agentic (OpenRouter/BYOK) or the Cursor pin, depending on resolved compute.
134
+ 5. **Persist** events, then **put** backend file artifacts through the object store (overflow pointer + stash when over the cap). Local path is kept if put fails.
135
+ 6. **Relay** Discord-safe `**Card**` progress (edited in place when possible) and a receipt that shows kind + jump URL (never a CDN URL, never hidden chain-of-thought).
136
+
137
+ ## Optional MCP Discord servers
138
+
139
+ Default I/O is REST. These adapters are optional. **Upstream source code is not copied.**
140
+
141
+ | Provider | Repository | License | Notes |
142
+ |----------|------------|---------|-------|
143
+ | **SaseQ / discord-mcp** | https://github.com/SaseQ/discord-mcp | MIT | HTTP endpoint convention via `SASEQ_MCP_HTTP_URL` (default `http://127.0.0.1:8085/mcp`). Prefer HTTP; stdio requires an explicit `DISCORD_MCP_STDIO_COMMAND` (no fabricated npm default). |
144
+ | **BrainDAO / mcp-discord** (`@iqai`) | https://github.com/BrainDAO/mcp-discord | MIT | Sampling/tool convention exposed as an adapter seam; **one Gateway owner per bot token** — no second Gateway required. Stdio requires an explicit `DISCORD_MCP_STDIO_COMMAND` such as `npx -y @iqai/mcp-discord`. |
145
+
146
+ See [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) for details.
147
+
148
+ ## Dev / fake path
149
+
150
+ ```bash
151
+ # Requires Python 3.11+
152
+ python -m venv .venv
153
+ source .venv/bin/activate
154
+ pip install -e ".[dev]"
155
+
156
+ cp .env.example .env
157
+ # DISCORD_BOT_TOKEN = Bot token (xxx.yyy.zzz), never Application ID or OAuth secret
158
+
159
+ discord-os bootstrap
160
+ discord-os check
161
+
162
+ # Dry-run (fake Discord + fake Puppetmaster — no network)
163
+ discord-os run "Summarize open items" --channel-id 123 --fake --no-discord-post
164
+ discord-os put ./notes.bin --channel-id 123 --fake --json
165
+ discord-os get MESSAGE_ID --channel-id 123 --out ./got.bin --fake --json
166
+ discord-os listen --channel-id 123 --fake --once
167
+ discord-os connect --provider openrouter --from-env --json
168
+ discord-os status --json
169
+ ```
170
+
171
+ ### Discord providers
172
+
173
+ - Default: `DISCORD_MCP_PROVIDER=rest` (official API, no MCP process)
174
+ - Optional: `saseq` or `braindao` plus `DISCORD_MCP_TRANSPORT=http` or `stdio`
175
+ - HTTP URLs: `SASEQ_MCP_HTTP_URL` (default `http://127.0.0.1:8085/mcp`) / `BRAINDAO_MCP_HTTP_URL`
176
+ - Stdio: `DISCORD_MCP_STDIO_COMMAND` is **required** when `transport=stdio`
177
+ - `DISCORD_MAX_OBJECT_BYTES` (optional; default `10485760`)
178
+
179
+ MCP catalog discovery is runtime-only. Missing file tools fall back to REST. CDN URLs stay ephemeral.
180
+
181
+ ### Compute and keys
182
+
183
+ | Path | How the key arrives | What Discord sees |
184
+ |------|---------------------|-------------------|
185
+ | Host inherit | `OPENROUTER_API_KEY` already on the listen host; `/connect` or `connect --from-env` | Fingerprint only |
186
+ | Pairing ticket | `/connect` with no secret mints an 8-char ticket (15 min); paste the key on host stdin | Ticket code only |
187
+ | Shred absorb | `/connect <secret>` or `!connect <secret>` | Payload once, then delete; card is fingerprint only |
188
+
189
+ Vault files live under `{workspace}/keys/` (ciphertext + `master.key`). Never commit them. Agentic dispatch injects the key into the **subprocess env** as `OPENROUTER_API_KEY` — never argv, never logs.
190
+
191
+ ### Host surfaces (poverty default)
192
+
193
+ Discord is the remote. The listen host opens local surfaces the same way a desktop harness does:
194
+
195
+ | Verb | What opens |
196
+ |------|------------|
197
+ | `/open` or `!open` (default: files) | File manager at the workspace-relative path |
198
+ | `/open terminal [path]` | Terminal at that path |
199
+ | `/open files [path]` | File manager |
200
+ | `/open browser <url>` or `/open https://…` | Browser, allowlisted only |
201
+
202
+ Paths stay inside `PUPPETMASTER_CWD` and the workspace. `~` is rejected. Browser URLs are loopback `http(s)` to `127.0.0.1` or `localhost`, or Discord channel jump links (`https://discord.com/channels/…`). Same engine: `discord-os open terminal|files|browser`.
203
+
204
+ Slash chrome is **opt-in** and does not replace `listen`. Set `AGENT_DISCORD_INTERACTIONS=http`, install `pip install 'discord-os[interactions]'` for Ed25519 verify, then `discord-os interactions --register --guild-id ID` and `--serve`. Bind is loopback (`127.0.0.1:8743`). If you want Discord to POST Interactions, you tunnel that URL and paste **your** public HTTPS URL into Developer Portal → Interactions Endpoint URL. Do not paste a tunnel URL into chat. Slash `/connect` has **no secret option** — inherit, ticket, or host CLI only. This does not open a second Gateway.
205
+
206
+ ### Puppetmaster model pin
207
+
208
+ | Field | Value |
209
+ |-------|-------|
210
+ | Compute default | `AGENT_DISCORD_COMPUTE=auto` |
211
+ | Agentic canonical / adapter | `openrouter/auto` |
212
+ | Canonical Cursor model (receipts/audit) | `cursor/grok-4-5` |
213
+ | Cursor adapter (`puppetmaster cursor --model`) | `grok-4.5` |
214
+ | Cursor allowlist | **only** `cursor/grok-4-5` |
215
+ | Agentic allowlist | **only** `openrouter/auto` |
216
+
217
+ Requests for any other model raise an error. There is **no** silent remap. Cursor compute still invokes `puppetmaster cursor …`. Agentic compute invokes `puppetmaster agentic … --provider openrouter --mode implement`. Set `PUPPETMASTER_CWD` to control `--cwd`.
218
+
219
+ ### Optional Marionette backend
220
+
221
+ Default `AGENT_DISCORD_BACKEND=puppetmaster`. To opt in:
222
+
223
+ ```bash
224
+ AGENT_DISCORD_BACKEND=marionette
225
+ MARIONETTE_BASE_URL=http://127.0.0.1:8787 # your local Marionette HTTP API
226
+ # Optional path overrides (defaults shown):
227
+ # MARIONETTE_SESSIONS_PATH=/v1/sessions
228
+ # MARIONETTE_JOBS_PATH=/v1/jobs
229
+ ```
230
+
231
+ The adapter documents an expected session/job/events/status/cancel contract; it does **not** pretend an unverified endpoint is guaranteed. Missing `MARIONETTE_BASE_URL` or transport failures surface as configuration/transport errors.
232
+
233
+ ## CLI
234
+
235
+ ```text
236
+ discord-os bootstrap [--workspace PATH]
237
+ discord-os check [--allow-empty-token] [--live] [--channel-id ID]
238
+ discord-os run TASK --channel-id ID [--message-id ID] [--fake] [--no-discord-post] [--json]
239
+ discord-os setup --channel-id ID
240
+ discord-os host start --channel-id ID
241
+ discord-os host stop
242
+ discord-os host status
243
+ discord-os listen --channel-id ID [--once] [--interval SEC] [--fake] [--json]
244
+ discord-os connect [--provider openrouter] [--ticket T] [--from-env] [--json]
245
+ discord-os status [--json]
246
+ discord-os invite [--application-id ID] [--json]
247
+ discord-os open {terminal,files,browser} [PATH_OR_URL] [--json]
248
+ discord-os interactions [--register] [--guild-id ID] [--serve] [--json]
249
+ discord-os put PATH --channel-id ID [--thread-id ID] [--guild-id ID] [--kind blob] [--fake] [--json]
250
+ discord-os get MESSAGE_ID --channel-id ID [--attachment-id ID] [--out PATH] [--fake] [--json]
251
+ discord-os ls --channel-id ID [--run-id ID] [--fake] [--json]
252
+ ```
253
+
254
+ `put` / `get` / `ls --fake` need no network. `get` writes bytes to `--out`, or to stdout only when stdout is not a tty (otherwise `--out` is required). Pointer JSON never includes a `url` key.
255
+
256
+ Also: `python -m agent_discord …`
257
+
258
+ ## Architecture (small & readable)
259
+
260
+ ```text
261
+ CLI → Orchestrator → backend (Puppetmaster agentic | Puppetmaster cursor | optional Marionette HTTP | fake)
262
+ ↘ SQLite (bindings, tasks, runs, events, memory, artifacts + object pointers,
263
+ inbound message dedupe, gateway ownership,
264
+ optional research claims / leases / negatives)
265
+ ↘ Discord facade → object store → REST (default) | optional SaseQ/BrainDAO | fake
266
+ ```
267
+
268
+ Message intake is REST. The host process opens a Discord Gateway only for On/Off buttons. The SQLite gateway row is a local one-process lock and is stolen if the previous owner pid is dead.
269
+
270
+ - **stdlib-first** core; optional `pytest` for development.
271
+ - Explicit typed contracts + dependency injection — tests never need Discord, Cursor, or network.
272
+ - Durable object key is `DiscordObjectRef` (channel / message / attachment / sha256). Channel id is the ACL; `get` refuses a mismatched caller channel (confused-deputy).
273
+ - Gateway exclusivity is **durable** in SQLite across concurrent local processes (in-memory registry retained for unit tests).
274
+ - Inbound Discord message IDs are deduplicated at orchestration level (prior receipt reused or explicit ignored-duplicate result).
275
+ - Event/artifact payloads recursively strip forbidden hidden-reasoning keys.
276
+ - BrainDAO sampling-compatible ingress is an adapter seam on the same facade.
277
+ - **Research memory** (optional orchestration context): typed claims with deterministic fingerprints, atomic leases, provenance/evidence, and queryable negative findings. Ordinary memory recall is unchanged; research metadata is not required for normal tasks.
278
+ - **Marionette backend** (optional, explicit opt-in via `AGENT_DISCORD_BACKEND=marionette`): stdlib urllib adapter with configurable endpoint paths and injectable transport for tests. Default remains Puppetmaster. Unconfigured/unavailable Marionette fails closed — no silent fallback. Model field still carries the canonical pin `cursor/grok-4-5` → adapter `grok-4.5`.
279
+
280
+ ## Development / tests
281
+
282
+ ```bash
283
+ pip install -e ".[dev]"
284
+ pytest
285
+ ```
286
+
287
+ Tests use fake MCP, Puppetmaster, and Marionette providers only.
288
+
289
+ ## License
290
+
291
+ MIT — see [`LICENSE`](LICENSE).
@@ -0,0 +1,264 @@
1
+ # Discord OS
2
+
3
+ Discord is the screen. This process is the computer. Your phone is the remote.
4
+
5
+ This is **your** bot on **your** machine. There is no hosted fleet to invite. Leave the host running; turn work on and off from Discord.
6
+
7
+ The GitHub repo is [`professorpalmer/agent-discord`](https://github.com/professorpalmer/agent-discord). The `agent-discord` command still works. Env vars and the `.agent-discord` workspace directory stay as they are.
8
+
9
+ ## 1. Make a Discord bot
10
+
11
+ Do this once in a browser.
12
+
13
+ 1. Open the [Discord Developer Portal](https://discord.com/developers/applications) and sign in.
14
+ 2. **New Application**. Name it whatever you want (this is the bot people will see).
15
+ 3. Left sidebar → **Bot**.
16
+ 4. **Reset Token** / **Copy**. The token looks like `xxx.yyy.zzz`. That is `DISCORD_BOT_TOKEN`.
17
+ - Do **not** use Application ID.
18
+ - Do **not** use the OAuth client secret.
19
+ 5. On the same Bot page, enable **Message Content Intent**. Save changes.
20
+ 6. Left sidebar → **General Information**. Copy **Application ID** (digits only). That is `DISCORD_APPLICATION_ID`.
21
+ 7. In Discord, create or pick a private staff channel. Copy its channel ID (Developer Mode → right-click the channel → Copy Channel ID).
22
+
23
+ ## 2. Install once on the machine that will do the work
24
+
25
+ Python 3.11+. From a clone of `dev` today (`pip install discord-os` after PyPI):
26
+
27
+ ```bash
28
+ python3 -m venv .venv
29
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
30
+ pip install -e .
31
+ pip install puppetmaster-ai # compute kernel; not bundled
32
+ export OPENROUTER_API_KEY=... # never commit
33
+ discord-os bootstrap
34
+ ```
35
+
36
+ Put these in `.env` (or write the bot token to `~/.pmharness/.discord_token`, mode 0600):
37
+
38
+ ```bash
39
+ DISCORD_BOT_TOKEN=xxx.yyy.zzz
40
+ DISCORD_APPLICATION_ID=123456789012345678
41
+ ```
42
+
43
+ Then one command:
44
+
45
+ ```bash
46
+ discord-os setup --channel-id YOUR_CHANNEL_ID
47
+ ```
48
+
49
+ That invites the bot (open the printed URL), installs a login helper so it comes back after reboot, and posts a HOST card in the channel with **On** and **Off** buttons. You do not type commands in Discord after this.
50
+
51
+ ## 3. Use it from Discord
52
+
53
+ | In Discord | What happens |
54
+ |---|---|
55
+ | **On** | Starts work on the host. Type a normal sentence as a task. |
56
+ | **Off** | Stops work. The helper stays so On still works from your phone. |
57
+ | a normal sentence | A task, only while On. |
58
+
59
+ If Discord logs the bot out, work stops. The login helper starts again idle.
60
+
61
+ No Docker. No slash commands. No public URL. No `/on` to remember.
62
+
63
+ Default I/O is Discord REST. SaseQ/BrainDAO MCP is optional if you already run those servers.
64
+
65
+ ## Thesis
66
+
67
+ Discord already is the phone UI, the ACL (a channel you control), the notification bus, and the identity layer. Discord OS treats **artifacts as Discord objects**:
68
+
69
+ ```text
70
+ object bytes
71
+ → attachment + caption on a channel message
72
+ → Discord CDN (ephemeral signed URL, ~24h)
73
+ → durable key = channel_id / message_id / attachment_id (+ sha256)
74
+ → retrieve later by re-fetching the message (fresh URL), then downloading
75
+ ```
76
+
77
+ Never persist a CDN URL as the durable key. Official retrieve is Get Channel Message (or `POST /attachments/refresh-urls`). Jump links (`https://discord.com/channels/{guild|@me}/{channel}/{message}`) are what we print for humans.
78
+
79
+ ## Prior art (steal economics, not product)
80
+
81
+ Two piles exist. We are neither.
82
+
83
+ | Pile | Examples | What they do | What we take | What we refuse |
84
+ |------|----------|--------------|--------------|----------------|
85
+ | Discord-as-disk | DiscordFS, forscht/ddrive, KITdt/discord-drive, missuo/discord-image | Chunk files into channels as free S3 / WebDav | Snowflake ID pointer; refresh-on-get | Unlimited chunked S3, public CDN, WebDav, 4TB multipart |
86
+ | Discord-as-agent-relay | Discode, Agent4Discord, Agentboard, cursor-mobile-bridge, claudecode-discord | Channel=workspace, thread=run, phone UI; artifacts stay on local disk/tmux | Channel as workspace / ACL; phone-native receipts | Relaying progress while leaving blobs on disk only |
87
+
88
+ Discord has said: if you host files on Discord, find a more suitable service. Honest S3 replacement here means **agent artifacts under Discord size limits in your own staff channel**. It does not mean a public CDN.
89
+
90
+ Default live I/O is **Discord REST** with the Bot token (`DISCORD_BOT_TOKEN` or `~/.pmharness/.discord_token`). That value is the Bot token (`xxx.yyy.zzz`) from the Bot tab — not the Application client ID and not the OAuth client secret. Optional SaseQ/BrainDAO MCP adapters try file-tool names first; if the catalog has no file tool they fall back to the same REST path. We still refuse to base64-dump files into `send_message`. The fake provider is the hermetic proof of the protocol. Foreground `listen` does **not** open a Discord Gateway. `host` / `setup` opens a Gateway **only** so On/Off buttons work — no public URL. Do not run that beside another bot process that already owns the Gateway.
91
+
92
+ ## Honest limits
93
+
94
+ - Default object cap is **10 MiB** (`DISCORD_MAX_OBJECT_BYTES=10485760`). Discord free is roughly 10–25MB; Nitro is higher. Configurable, not unlimited.
95
+ - Discord ToS: conversation artifacts in **your** server, not a public CDN or anonymous disk.
96
+ - Not a compute host. Default compute is `AGENT_DISCORD_COMPUTE=auto`: Puppetmaster **agentic** (`openrouter/auto`) when an OpenRouter key is on the host or in the workspace vault; otherwise the Cursor pin (`cursor/grok-4-5` / adapter `grok-4.5`). **No silent model fallback.**
97
+ - No DiscordFS-style multipart chunking. Oversize artifacts become an `overflow` pointer + local stash, not multipart CDN objects.
98
+ - `/connect <secret>` and `/open` are message-prefix verbs by default, not Discord slash Interactions. Discord still sees shred payloads once before delete.
99
+ - `listen` ignores channel history older than a durable per-channel SQLite watermark (first listen: now minus 15s, same slack as process start; later processes resume from the stored high-water, including skipped cards/connects/opens) so a seeded staff channel is not dispatched as an implement job.
100
+
101
+ ## What a run does
102
+
103
+ 1. **Connect** (optional): `/connect` on the listen host inherits `OPENROUTER_API_KEY`, shreds a pasted secret after delete, or mints a pairing ticket for `discord-os connect --ticket`.
104
+ 2. **Intake** a natural-language task from `run` or the host loop (staff channel / phone). Cards, receipts, and object-store captions are skipped. On/Off buttons, `/connect`, and `/open` are intercepted before task dispatch. Work is accepted only while On. Discord is the remote; this process opens Terminal, the file manager, or an allowlisted browser on the host.
105
+ 3. **Snapshot** scoped context from SQLite memory + channel bindings.
106
+ 4. **Dispatch** to Puppetmaster agentic (OpenRouter/BYOK) or the Cursor pin, depending on resolved compute.
107
+ 5. **Persist** events, then **put** backend file artifacts through the object store (overflow pointer + stash when over the cap). Local path is kept if put fails.
108
+ 6. **Relay** Discord-safe `**Card**` progress (edited in place when possible) and a receipt that shows kind + jump URL (never a CDN URL, never hidden chain-of-thought).
109
+
110
+ ## Optional MCP Discord servers
111
+
112
+ Default I/O is REST. These adapters are optional. **Upstream source code is not copied.**
113
+
114
+ | Provider | Repository | License | Notes |
115
+ |----------|------------|---------|-------|
116
+ | **SaseQ / discord-mcp** | https://github.com/SaseQ/discord-mcp | MIT | HTTP endpoint convention via `SASEQ_MCP_HTTP_URL` (default `http://127.0.0.1:8085/mcp`). Prefer HTTP; stdio requires an explicit `DISCORD_MCP_STDIO_COMMAND` (no fabricated npm default). |
117
+ | **BrainDAO / mcp-discord** (`@iqai`) | https://github.com/BrainDAO/mcp-discord | MIT | Sampling/tool convention exposed as an adapter seam; **one Gateway owner per bot token** — no second Gateway required. Stdio requires an explicit `DISCORD_MCP_STDIO_COMMAND` such as `npx -y @iqai/mcp-discord`. |
118
+
119
+ See [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) for details.
120
+
121
+ ## Dev / fake path
122
+
123
+ ```bash
124
+ # Requires Python 3.11+
125
+ python -m venv .venv
126
+ source .venv/bin/activate
127
+ pip install -e ".[dev]"
128
+
129
+ cp .env.example .env
130
+ # DISCORD_BOT_TOKEN = Bot token (xxx.yyy.zzz), never Application ID or OAuth secret
131
+
132
+ discord-os bootstrap
133
+ discord-os check
134
+
135
+ # Dry-run (fake Discord + fake Puppetmaster — no network)
136
+ discord-os run "Summarize open items" --channel-id 123 --fake --no-discord-post
137
+ discord-os put ./notes.bin --channel-id 123 --fake --json
138
+ discord-os get MESSAGE_ID --channel-id 123 --out ./got.bin --fake --json
139
+ discord-os listen --channel-id 123 --fake --once
140
+ discord-os connect --provider openrouter --from-env --json
141
+ discord-os status --json
142
+ ```
143
+
144
+ ### Discord providers
145
+
146
+ - Default: `DISCORD_MCP_PROVIDER=rest` (official API, no MCP process)
147
+ - Optional: `saseq` or `braindao` plus `DISCORD_MCP_TRANSPORT=http` or `stdio`
148
+ - HTTP URLs: `SASEQ_MCP_HTTP_URL` (default `http://127.0.0.1:8085/mcp`) / `BRAINDAO_MCP_HTTP_URL`
149
+ - Stdio: `DISCORD_MCP_STDIO_COMMAND` is **required** when `transport=stdio`
150
+ - `DISCORD_MAX_OBJECT_BYTES` (optional; default `10485760`)
151
+
152
+ MCP catalog discovery is runtime-only. Missing file tools fall back to REST. CDN URLs stay ephemeral.
153
+
154
+ ### Compute and keys
155
+
156
+ | Path | How the key arrives | What Discord sees |
157
+ |------|---------------------|-------------------|
158
+ | Host inherit | `OPENROUTER_API_KEY` already on the listen host; `/connect` or `connect --from-env` | Fingerprint only |
159
+ | Pairing ticket | `/connect` with no secret mints an 8-char ticket (15 min); paste the key on host stdin | Ticket code only |
160
+ | Shred absorb | `/connect <secret>` or `!connect <secret>` | Payload once, then delete; card is fingerprint only |
161
+
162
+ Vault files live under `{workspace}/keys/` (ciphertext + `master.key`). Never commit them. Agentic dispatch injects the key into the **subprocess env** as `OPENROUTER_API_KEY` — never argv, never logs.
163
+
164
+ ### Host surfaces (poverty default)
165
+
166
+ Discord is the remote. The listen host opens local surfaces the same way a desktop harness does:
167
+
168
+ | Verb | What opens |
169
+ |------|------------|
170
+ | `/open` or `!open` (default: files) | File manager at the workspace-relative path |
171
+ | `/open terminal [path]` | Terminal at that path |
172
+ | `/open files [path]` | File manager |
173
+ | `/open browser <url>` or `/open https://…` | Browser, allowlisted only |
174
+
175
+ Paths stay inside `PUPPETMASTER_CWD` and the workspace. `~` is rejected. Browser URLs are loopback `http(s)` to `127.0.0.1` or `localhost`, or Discord channel jump links (`https://discord.com/channels/…`). Same engine: `discord-os open terminal|files|browser`.
176
+
177
+ Slash chrome is **opt-in** and does not replace `listen`. Set `AGENT_DISCORD_INTERACTIONS=http`, install `pip install 'discord-os[interactions]'` for Ed25519 verify, then `discord-os interactions --register --guild-id ID` and `--serve`. Bind is loopback (`127.0.0.1:8743`). If you want Discord to POST Interactions, you tunnel that URL and paste **your** public HTTPS URL into Developer Portal → Interactions Endpoint URL. Do not paste a tunnel URL into chat. Slash `/connect` has **no secret option** — inherit, ticket, or host CLI only. This does not open a second Gateway.
178
+
179
+ ### Puppetmaster model pin
180
+
181
+ | Field | Value |
182
+ |-------|-------|
183
+ | Compute default | `AGENT_DISCORD_COMPUTE=auto` |
184
+ | Agentic canonical / adapter | `openrouter/auto` |
185
+ | Canonical Cursor model (receipts/audit) | `cursor/grok-4-5` |
186
+ | Cursor adapter (`puppetmaster cursor --model`) | `grok-4.5` |
187
+ | Cursor allowlist | **only** `cursor/grok-4-5` |
188
+ | Agentic allowlist | **only** `openrouter/auto` |
189
+
190
+ Requests for any other model raise an error. There is **no** silent remap. Cursor compute still invokes `puppetmaster cursor …`. Agentic compute invokes `puppetmaster agentic … --provider openrouter --mode implement`. Set `PUPPETMASTER_CWD` to control `--cwd`.
191
+
192
+ ### Optional Marionette backend
193
+
194
+ Default `AGENT_DISCORD_BACKEND=puppetmaster`. To opt in:
195
+
196
+ ```bash
197
+ AGENT_DISCORD_BACKEND=marionette
198
+ MARIONETTE_BASE_URL=http://127.0.0.1:8787 # your local Marionette HTTP API
199
+ # Optional path overrides (defaults shown):
200
+ # MARIONETTE_SESSIONS_PATH=/v1/sessions
201
+ # MARIONETTE_JOBS_PATH=/v1/jobs
202
+ ```
203
+
204
+ The adapter documents an expected session/job/events/status/cancel contract; it does **not** pretend an unverified endpoint is guaranteed. Missing `MARIONETTE_BASE_URL` or transport failures surface as configuration/transport errors.
205
+
206
+ ## CLI
207
+
208
+ ```text
209
+ discord-os bootstrap [--workspace PATH]
210
+ discord-os check [--allow-empty-token] [--live] [--channel-id ID]
211
+ discord-os run TASK --channel-id ID [--message-id ID] [--fake] [--no-discord-post] [--json]
212
+ discord-os setup --channel-id ID
213
+ discord-os host start --channel-id ID
214
+ discord-os host stop
215
+ discord-os host status
216
+ discord-os listen --channel-id ID [--once] [--interval SEC] [--fake] [--json]
217
+ discord-os connect [--provider openrouter] [--ticket T] [--from-env] [--json]
218
+ discord-os status [--json]
219
+ discord-os invite [--application-id ID] [--json]
220
+ discord-os open {terminal,files,browser} [PATH_OR_URL] [--json]
221
+ discord-os interactions [--register] [--guild-id ID] [--serve] [--json]
222
+ discord-os put PATH --channel-id ID [--thread-id ID] [--guild-id ID] [--kind blob] [--fake] [--json]
223
+ discord-os get MESSAGE_ID --channel-id ID [--attachment-id ID] [--out PATH] [--fake] [--json]
224
+ discord-os ls --channel-id ID [--run-id ID] [--fake] [--json]
225
+ ```
226
+
227
+ `put` / `get` / `ls --fake` need no network. `get` writes bytes to `--out`, or to stdout only when stdout is not a tty (otherwise `--out` is required). Pointer JSON never includes a `url` key.
228
+
229
+ Also: `python -m agent_discord …`
230
+
231
+ ## Architecture (small & readable)
232
+
233
+ ```text
234
+ CLI → Orchestrator → backend (Puppetmaster agentic | Puppetmaster cursor | optional Marionette HTTP | fake)
235
+ ↘ SQLite (bindings, tasks, runs, events, memory, artifacts + object pointers,
236
+ inbound message dedupe, gateway ownership,
237
+ optional research claims / leases / negatives)
238
+ ↘ Discord facade → object store → REST (default) | optional SaseQ/BrainDAO | fake
239
+ ```
240
+
241
+ Message intake is REST. The host process opens a Discord Gateway only for On/Off buttons. The SQLite gateway row is a local one-process lock and is stolen if the previous owner pid is dead.
242
+
243
+ - **stdlib-first** core; optional `pytest` for development.
244
+ - Explicit typed contracts + dependency injection — tests never need Discord, Cursor, or network.
245
+ - Durable object key is `DiscordObjectRef` (channel / message / attachment / sha256). Channel id is the ACL; `get` refuses a mismatched caller channel (confused-deputy).
246
+ - Gateway exclusivity is **durable** in SQLite across concurrent local processes (in-memory registry retained for unit tests).
247
+ - Inbound Discord message IDs are deduplicated at orchestration level (prior receipt reused or explicit ignored-duplicate result).
248
+ - Event/artifact payloads recursively strip forbidden hidden-reasoning keys.
249
+ - BrainDAO sampling-compatible ingress is an adapter seam on the same facade.
250
+ - **Research memory** (optional orchestration context): typed claims with deterministic fingerprints, atomic leases, provenance/evidence, and queryable negative findings. Ordinary memory recall is unchanged; research metadata is not required for normal tasks.
251
+ - **Marionette backend** (optional, explicit opt-in via `AGENT_DISCORD_BACKEND=marionette`): stdlib urllib adapter with configurable endpoint paths and injectable transport for tests. Default remains Puppetmaster. Unconfigured/unavailable Marionette fails closed — no silent fallback. Model field still carries the canonical pin `cursor/grok-4-5` → adapter `grok-4.5`.
252
+
253
+ ## Development / tests
254
+
255
+ ```bash
256
+ pip install -e ".[dev]"
257
+ pytest
258
+ ```
259
+
260
+ Tests use fake MCP, Puppetmaster, and Marionette providers only.
261
+
262
+ ## License
263
+
264
+ MIT — see [`LICENSE`](LICENSE).
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "discord-os"
7
+ version = "0.3.0"
8
+ description = "Discord OS: Discord is the harness UI for local agent work. Artifacts are Discord objects (snowflake IDs)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Discord OS contributors" }]
13
+ keywords = ["discord", "discord-os", "mcp", "puppetmaster", "agent", "cli"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Communications :: Chat",
23
+ ]
24
+ dependencies = []
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest>=8.0"]
28
+ interactions = ["PyNaCl>=1.5"]
29
+
30
+ [project.scripts]
31
+ discord-os = "agent_discord.cli:main"
32
+ agent-discord = "agent_discord.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/professorpalmer/agent-discord"
36
+ Attribution-SaseQ = "https://github.com/SaseQ/discord-mcp"
37
+ Attribution-BrainDAO = "https://github.com/BrainDAO/mcp-discord"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ pythonpath = ["src"]
45
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """Discord OS: Discord is the harness UI for local agent work."""
2
+
3
+ from __future__ import annotations
4
+
5
+ PRODUCT_NAME = "Discord OS"
6
+ CLI_NAME = "discord-os"
7
+ PACKAGE_NAME = "discord-os"
8
+ REPO_URL = "https://github.com/professorpalmer/agent-discord"
9
+ CLI_OWNER_PREFIX = "discord-os-cli-"
10
+ LEGACY_CLI_OWNER_PREFIX = "agent-discord-cli-"
11
+
12
+ __version__ = "0.3.0"
@@ -0,0 +1,3 @@
1
+ from agent_discord.cli import main
2
+
3
+ raise SystemExit(main())