@joe-broadhead/opencode-messaging 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,396 @@
1
+ # Configuration
2
+
3
+ Options go in the `options` object of the plugin's `plugins` entry in `opencode.json(c)`. Every
4
+ option is optional.
5
+
6
+ OpenCode does not validate plugin options. The plugin does, with a strict schema: an unknown or
7
+ misspelled key, or a value of the wrong type, fails plugin setup. The error names the key, for
8
+ example `opencode-messaging: invalid plugin options: daemon: Unrecognized key: "autostrat"`.
9
+
10
+ ```jsonc
11
+ {
12
+ "plugins": [
13
+ {
14
+ "package": "/Users/you/opencode-messaging",
15
+ "options": {
16
+ "home": "/Users/you/.messaging",
17
+ "daemon": { "autostart": true, "node": "/usr/local/bin/node" },
18
+ "session": { "name": "api-work" },
19
+ "inbound": { "wake": true, "notify": true, "wakeBudget": { "perPeer": 6, "windowMs": 3600000 } },
20
+ },
21
+ },
22
+ ],
23
+ }
24
+ ```
25
+
26
+ **The `/messaging` TUI takes these options from the server part.** OpenCode 2.2 loads the plugin's
27
+ TUI part without the `options` of `opencode.json(c)`. So the TUI never reads its own options, and never
28
+ resolves a home from the terminal's `$MESSAGING_HOME` or `~/.messaging`: it asks the plugin's server
29
+ part, over the plugin's RPC (`messaging.opencode`, method `config`), for the home it resolved and its
30
+ `daemon` options, and uses those. Nothing to repeat in `cli.json`; options there are ignored.
31
+
32
+ If the server part cannot be reached (it is not in `opencode.json(c)`, a bad option stopped it, or it
33
+ does not answer within 10 s), `/messaging` and `/messaging-status` say so
34
+ (`server_unavailable: …`) and do nothing: no connection, no daemon start, no file written, in any home.
35
+ The next command asks again. The live acceptance of 0.3.0 found the old fallback on OpenCode 2.2.5,
36
+ where a console started a daemon in `~/.messaging` while the configured home was elsewhere
37
+ (`docs/testing/live-acceptance.md`); the packaged-TUI smoke test now proves that path starts nothing.
38
+
39
+ ## Options
40
+
41
+ | Option | Type | Default | Meaning |
42
+ |---|---|---|---|
43
+ | `home` | absolute path | `$MESSAGING_HOME`, else `~/.messaging` | The Messaging home. The plugin's tools use the daemon's agent endpoint, `<home>/run/agent.sock` (on Windows the pointer file `<home>\run\agent.pipe`, which names a named pipe); the `/messaging` TUI its admin endpoint, `<home>/run/admin.sock` (`admin.pipe`). |
44
+ | `daemon.autostart` | boolean | `true` | Start `messagingd` when the socket does not answer and this machine has created or joined a team (`<home>/identity.json` exists). |
45
+ | `daemon.bin` | absolute path | this platform's `messagingd` executable, else `dist/messagingd.js` (see [which daemon runs](#which-daemon-runs)) | The daemon to run: a `messagingd` executable, or a `messagingd.js` (ending in `.js`, `.mjs` or `.cjs`), which runs under Node. |
46
+ | `daemon.node` | absolute path or a bare command on `PATH` | the host's own Node, else `node` on `PATH` | The Node executable (≥ 22.15) that runs `messagingd.js`. Setting it chooses `dist/messagingd.js` over the platform executable. |
47
+ | `session.name` | session name (`[a-z0-9][a-z0-9._-]{0,63}`) | slug of the OpenCode directory's basename | The address name: the session's address is `person/<name>@<label>`. The folder name is only the fallback. The daemon adds `-2`, `-3`, … when the name is taken on this machine. |
48
+ | `inbound.wake` | boolean | `true` | Wake an idle session for mail when the sender asks for it. This also selects the `push` delivery tier (`pull` when false). |
49
+ | `inbound.notify` | boolean | `true` | Post a transcript note for mail that does not wake the session, and for task changes. |
50
+ | `inbound.midTurn` | boolean | `true` | With `notify`: while the session works, put those notes into the result of its next tool call instead of holding them until the turn ends. |
51
+ | `inbound.wakeBudget.perPeer` | integer, 1–1000 | `6` | Wakes one teammate may cause in this OpenCode process per window. Over it, their mail gets a note instead. |
52
+ | `inbound.wakeBudget.windowMs` | integer, 60000–86400000 | `3600000` (1 h) | The sliding window of the wake budget, in milliseconds. |
53
+
54
+ ### `home`
55
+
56
+ The plugin uses the first one that is set: the `home` option, then the `MESSAGING_HOME` environment
57
+ variable of the OpenCode server, then `~/.messaging`. Every client on the machine (this plugin, the
58
+ `messaging` CLI, the MCP server) must use the same home to talk to the same daemon.
59
+
60
+ ### `daemon`
61
+
62
+ The plugin connects on first use: the first prompt in a session, or the first Messaging tool call.
63
+ Plugin setup never connects. If the endpoint does not answer, `autostart` is true and the machine
64
+ has joined a team (see [before a team](#before-the-machine-has-joined-a-team)), the plugin starts
65
+ the daemon.
66
+
67
+ #### Which daemon runs
68
+
69
+ The plugin picks the first that applies (`src/process/daemon.ts`, the one place that decides):
70
+
71
+ 1. **`daemon.bin`**, when you set it: a `.js`, `.mjs` or `.cjs` file runs under Node (below), anything
72
+ else is run as an executable.
73
+ 2. **`daemon.node`**, when you set it: the bundled `dist/messagingd.js` under that Node.
74
+ 3. **The platform executable.** The self-contained `messagingd` (the daemon compiled with Bun: no
75
+ Node, no install step) from this machine's package, which npm installs as one of the plugin's
76
+ `optionalDependencies`:
77
+
78
+ | Platform | Package | Executable |
79
+ |---|---|---|
80
+ | macOS arm64 | `@joe-broadhead/messaging-bin-darwin-arm64` | `bin/messagingd` |
81
+ | macOS x64 | `@joe-broadhead/messaging-bin-darwin-x64` | `bin/messagingd` |
82
+ | Linux arm64, glibc | `@joe-broadhead/messaging-bin-linux-arm64-gnu` | `bin/messagingd` |
83
+ | Linux x64, glibc | `@joe-broadhead/messaging-bin-linux-x64-gnu` | `bin/messagingd` |
84
+ | Windows x64 | `@joe-broadhead/messaging-bin-win32-x64` | `bin/messagingd.exe` (not code-signed; see [Windows](#windows)) |
85
+
86
+ Each is pinned at exactly the messaging release the plugin is built against (0.3.0), and npm
87
+ installs only the one whose `os` and `cpu` match. The plugin finds it from its own install
88
+ location the way Node resolves a package, and admits it only when the package is that version,
89
+ the executable is a regular file (not a link) with owner read and execute and no permission
90
+ outside 0755, and its SHA-256 is the one the plugin pins for that platform (the release's
91
+ `SHA256SUMS`). An installed package that fails a check is an error (`daemon_unavailable`, naming the
92
+ package and what failed), never a silent fallback. On Linux the plugin also checks for glibc
93
+ itself, because npm's lockfile does not record `libc`.
94
+ 4. **`dist/messagingd.js` under Node 22.15 or later**, when this platform has no package (musl,
95
+ Windows on arm64, any other platform) or npm did not install it (for
96
+ example `--omit=optional`, or a tarball unpacked without its platform package). The Node is the
97
+ executable OpenCode runs on, when that is plain Node 22.15 or newer (nothing is looked up on
98
+ `PATH`, so a `node` planted earlier on `PATH` is never run); otherwise `node` from OpenCode's
99
+ `PATH`, which the plugin first asks for its version (`node --version`). Stock OpenCode runs on Bun,
100
+ so this is the `PATH` case.
101
+ 5. Otherwise tools fail with `daemon_unavailable`, naming the platform, the package that is missing
102
+ or the Node that was found, and the remedy: reinstall the plugin so npm installs its optional
103
+ dependency, install Node 22.15 or later, or set `daemon.node` or `daemon.bin`.
104
+
105
+ Set `daemon.node` to an absolute path to avoid the `PATH` lookup in case 4.
106
+
107
+ The process has these properties:
108
+
109
+ - The process is detached and outlives OpenCode. On Windows it opens no console window.
110
+ - It has no stdio. Its `cwd` is the home, or your home directory if the Messaging home does not exist yet.
111
+ - `MESSAGING_HOME` is set to the home.
112
+ - It gets an allowlisted environment: `PATH`, `HOME`, user and locale variables, `TMPDIR`, `XDG_*`,
113
+ proxy variables, `NODE_EXTRA_CA_CERTS`, `MESSAGING_LOG_LEVEL` and `MESSAGING_IDENTITY_STORE` (on
114
+ Windows also `SystemRoot`, `USERPROFILE`, `TEMP` and the other variables Windows programs need).
115
+ Provider API keys, `NODE_OPTIONS` and other OpenCode secrets are not passed, and neither is any
116
+ `BUN_*` variable, which the platform executable (a Bun runtime) would otherwise read
117
+ (`BUN_BE_BUN=1` makes it act as `bun`). `messaging daemon start` uses the same list.
118
+ - The plugin never signals it. It is stopped with `messaging daemon stop` (`daemon.stop` over the admin
119
+ endpoint), on every platform.
120
+
121
+ The plugin then waits up to 8 s for the socket. One OpenCode process starts the daemon at most
122
+ once per home every 10 s. The daemon's own lock prevents a second instance.
123
+
124
+ One home has one daemon. The plugin starts its own only when nothing answers on the socket. A
125
+ daemon of another Messaging version that answers there (a `messaging` installed apart from the plugin,
126
+ or an older plugin's) is never replaced: tools fail with `daemon_mismatch`, naming both versions,
127
+ until you stop it with `messaging daemon stop`; the next tool call then starts the plugin's.
128
+
129
+ The plugin never starts the daemon where it runs as its own user (docs/self-hosting.md, "The
130
+ daemon under its own user"): when `MESSAGING_DAEMON_UID` names another uid, when the home belongs to
131
+ another user, or when the home is 0710, as `messagingd` leaves it for `ipc.agentGroup`. Tools then
132
+ fail with `daemon_unavailable` and say so; the operator starts the daemon. Windows has no uid or
133
+ mode, so there the plugin checks neither: the daemon checks the home's ACL itself, and the client
134
+ library refuses an endpoint another user could write. Windows has no separate-user layout, so a set
135
+ `MESSAGING_DAEMON_UID` still refuses.
136
+
137
+ #### Windows
138
+
139
+ The client libraries the plugin bundles speak the Windows daemon's endpoints: named pipes of random
140
+ names behind owner-only pointer files (`<home>\run\agent.pipe`, `admin.pipe`). The plugin registers
141
+ OpenCode's directory (`C:\…`) as the wake location as it is. The shell asks before `messaging.exe` and
142
+ `messagingd.exe` too.
143
+
144
+ On Windows x64 the plugin runs `messagingd.exe` from `@joe-broadhead/messaging-bin-win32-x64`
145
+ (case 3; no mode is checked there, Windows has none: the file's ACL is the installer's). It is not
146
+ code-signed, which matters on a person's machine:
147
+
148
+ - **Smart App Control** (Windows 11, when it is on in enforcement mode) blocks an unsigned executable
149
+ of unknown reputation however it arrived, with no per-app override, so `messagingd.exe` does not
150
+ start there until it is signed; tools then fail with `daemon_unavailable`. Set `daemon.node` to use
151
+ `dist/messagingd.js` under Node instead. Most machines that were not freshly installed, and managed
152
+ ones, have it off.
153
+ - **Microsoft Defender Antivirus** scans every executable; a new, unsigned one is more often the
154
+ subject of a heuristic detection. Report a detection to Microsoft as a false positive with the
155
+ sha256 from the messaging release's `SHA256SUMS`.
156
+ - **SmartScreen** assesses only files with a Mark of the Web. npm writes none, so an installed
157
+ package runs without a prompt.
158
+
159
+ If the connection drops, the plugin reconnects in the background with backoff (0.5 s, doubling
160
+ up to 30 s). If a session cannot register on its first prompt because the daemon is unreachable,
161
+ the plugin retries that registration in the background with the same backoff until it succeeds.
162
+ Without the retry, the session would get no mail and no wakes until your next prompt. Background
163
+ reconnects and retries never start the daemon, so a daemon you stopped stays stopped until the next
164
+ prompt or tool call. Once you start it yourself, waiting sessions register on their own. Set
165
+ `autostart: false` to manage `messagingd` yourself.
166
+
167
+ When the daemon cannot be reached, every tool fails with a `daemon_unavailable` error, whose remedy
168
+ says to start it.
169
+
170
+ #### Before the machine has joined a team
171
+
172
+ A daemon without a team refuses every call an agent can make, so the plugin does nothing with
173
+ one. Until `<home>/identity.json` exists (`messaging team create` or `messaging team join` writes it):
174
+
175
+ - **Prompts** do nothing beyond a file check: no connection, no daemon start, no note. If you
176
+ never use Messaging, you never see it.
177
+ - **Tools** fail with `no_identity`, whose remedy tells the model to ask you to set Messaging up.
178
+ Nothing starts the daemon, whatever `autostart` says. A daemon that is running but has no team is
179
+ reported the same way.
180
+ - **One setup note.** The first Messaging tool call that gets `no_identity` in a session holds one
181
+ transcript note for you (like the [inbound](#inbound) notes: no model turn, posted once the turn
182
+ ends). It gives the commands to run in a terminal: `messaging daemon start`, then
183
+ `messaging team create …` or `messaging team join - --label <machine>` with an invite ticket from a team
184
+ admin (`messaging team invite <you>`). The commands carry `--home <home>` when the plugin's home is
185
+ not `~/.messaging`. A session gets at most one such note. Its id is fixed per session, so the host
186
+ keeps only the first even after a plugin reload. Prompts never post it.
187
+ - **Joining needs no reload.** The plugin checks for the file on every prompt and tool call. After
188
+ `messaging team join`, the next call registers the session. With `autostart` on, it also starts
189
+ the daemon if it is not running.
190
+
191
+ The plugin only checks that `identity.json` exists. It never opens the file, so it never holds the
192
+ machine's keys, and the daemon remains the judge of the file's contents. `autostart` stays a
193
+ boolean: starting a daemon that has no team would not help an agent, and `messaging daemon start`
194
+ covers creating and joining.
195
+
196
+ #### The `/messaging` console
197
+
198
+ The TUI part uses the server part's `home`, `daemon.autostart`, `daemon.node` and `daemon.bin`,
199
+ over the plugin's RPC (see the note at the top of this page), validated with the same schema. A bad
200
+ option stops the server part, and the console then says it cannot reach it. It never starts the
201
+ daemon just to connect. With `autostart` on, it starts it, with the same `<node> <bin>` rule, to
202
+ create, join or ask to join a team once the input is complete, to use
203
+ [Messaging by Jelly](#messaging-by-jelly) once it has written the realm, and for other actions only
204
+ once this machine has joined a team or has a realm. With `autostart: false` it never starts it and tells you to run
205
+ `messaging daemon start`. Once this machine is in a team, the TUI keeps one connection for chat
206
+ notices and reconnects when the daemon restarts; it never starts the daemon for that.
207
+
208
+ ### `session.name`
209
+
210
+ Each OpenCode session registers as its own Messaging session with the key `opencode:<sessionID>`.
211
+ Its address is `person/<name>@<label>`, with `<name>` from this setting; the slug of the folder
212
+ name is only the fallback. Set it when the folder does not say what the session is for (a Jelly
213
+ agent's folder is always `workspace`). Peer sessions never register: they use the address of the
214
+ session the teammate wrote to. All sessions in one directory request the same name, so
215
+ the second becomes `name-2`. Registration repeats after a reconnect and asks for the name the
216
+ session already holds, so its address stays stable. The plugin never unregisters a session. The
217
+ daemon keeps a session addressable for 24 h after its client goes away.
218
+
219
+ ### `inbound`
220
+
221
+ | `wake` | `notify` | Session idle, sender asked to wake | Otherwise |
222
+ |---|---|---|---|
223
+ | true | true | wake prompt | transcript note |
224
+ | true | false | wake prompt | nothing (the agent pulls with `inbox`) |
225
+ | false | true | transcript note | transcript note |
226
+ | false | false | nothing | nothing |
227
+
228
+ - A **wake prompt** starts a model turn. Its text is a line written by the plugin, followed by the
229
+ framed message. Wakes that arrive within 2 s of the first are combined into one prompt that lists
230
+ every item with a framed preview; the agent reads the ones it wants.
231
+ - The **wake budget** limits the turns one teammate can start. Each person gets
232
+ `wakeBudget.perPeer` wakes per sliding `wakeBudget.windowMs` (default 6 per hour), counted across
233
+ all sessions of this OpenCode process. The key is the sender's person name, not its session
234
+ address, because a sender can open as many sessions as it likes. A combined prompt costs each
235
+ sender in it one wake, counted when its first item enters the 2 s window. Over budget, the item is
236
+ handled as if it did not ask to wake: a transcript note when `notify` is on, otherwise nothing.
237
+ The daemon's loop guard does not limit wakes; see [security.md](security.md#residual-risks).
238
+ - A **transcript note** (`session.synthetic` with `resume: false`, `delivery: "queue"`) never
239
+ starts a turn. The model sees it during the session's next turn. Notes are posted only while
240
+ the session is idle. Mail that arrives during a turn goes into the next tool result (see
241
+ `midTurn`) or else waits until the turn ends, and anything the agent has read or listed by then
242
+ is dropped from the note.
243
+ - **`midTurn`** (default `true`, only with `notify`): mail and task notes that arrive while the
244
+ session works go into the result of its next tool call, after the tool's own output, instead of
245
+ waiting for the turn to end. At most 5 items and 7,168 characters per tool result; the rest
246
+ follow in later results or the idle note. A call that ran a Messaging tool gets none. An item counts
247
+ as handed to the agent only once OpenCode has recorded the result with the note in it. Set it to
248
+ `false` to keep every tool result exactly as the tool returned it.
249
+ - A wake prompt is acknowledged only after OpenCode has admitted it. If it fails, the plugin posts
250
+ a note instead, whatever `notify` says, and acknowledges the item once the note is posted. A
251
+ crash in between leaves the item unread, and the next catch-up shows it.
252
+ - A **wake from another person never runs in this session**. It runs in a peer session; see
253
+ [peer turns](#peer-turns). Mail from your own other sessions wakes this session as before, and
254
+ mail that arrives while this session works still goes into its next tool result.
255
+ - A session counts as busy from a prompt until OpenCode reports that the prompt's work has ended,
256
+ so no note is queued into a turn that is about to start.
257
+ - `notify` also controls task notes: a requester hears when a task ends (completed, failed,
258
+ rejected or cancelled) or waits for input or auth; an executor hears when the requester cancels.
259
+ When a task ends, the requester's note also names its follow-up messages the executor did not
260
+ read. The `task` tool shows every follow-up with its stage.
261
+ Only peer-caused changes produce notes; this session's own updates do not.
262
+ - Mail that arrived while the session was not subscribed is rolled into one catch-up note when the
263
+ session registers or reconnects, posted at the next idle.
264
+
265
+ The daemon has its own policy: `inbound.wake` in `$MESSAGING_HOME/config.json` (`"team"` or
266
+ `"never"`). A wake happens only when both allow it.
267
+
268
+ With `inbound.wake` on, each session registers a `host`: this location's directory, the OpenCode
269
+ session id, and the server's pid. If OpenCode unloads the plugin while wake mail arrives, the
270
+ daemon can ask that server to load this location again and call `messaging.opencode/wake`. Turn the
271
+ daemon's `wake.bridge` off to stop that. The plugin still decides each item with the idle check,
272
+ the wake budget and coalescing above.
273
+
274
+ ### Peer turns
275
+
276
+ When a teammate's agent wakes a session, the plugin runs that turn in a **peer session**, never in
277
+ the working session the teammate addressed:
278
+
279
+ - One peer session per working session and teammate (person), titled `Messaging: <person>`. The
280
+ plugin creates it on the first wake and reuses it for later ones. Its tools use the working
281
+ session's Messaging address, so replies come from the address the teammate wrote to. It never
282
+ registers an address of its own.
283
+ - Each wake prompt is tagged in its metadata with `turn: "peer"`, the `peer` person, and the
284
+ envelope (`envelopeId`, or `envelopeIds` for a combined prompt) and task ids.
285
+ - Unless the daemon says the wake is `trusted`, the turn is **guarded**: it can read the project and
286
+ write only under `.messaging-peers/<person>/` in this directory. Its tools are read, glob, grep,
287
+ edit, write, skill and Code Mode; it cannot run the shell, use the web or another plugin's tools,
288
+ start subagents, ask questions, or reach outside the project. Messaging sends go only back to that
289
+ person, and Messaging reads show only what concerns them. Nothing waits for you: a step that would ask is refused, and
290
+ the model is told to say so in its reply. [security.md](security.md#peer-turns) has the details.
291
+ - The plugin creates `.messaging-peers/` with a `.gitignore` that ignores everything in it.
292
+ - Your own other sessions (the same person) are not peers: their wakes run in the working session.
293
+
294
+ **Trust is a grant, not an option.** You trust a person with `messaging grant <person> --trust 30d`
295
+ (or `/messaging` → People): the daemon then marks their items `consent: "trusted"` until the grant
296
+ expires (spec §6.9; at most `LIMITS.trustMaxMs`). A wake whose every item is `trusted` runs
297
+ unguarded in their peer session, with the same rights as the session they addressed, under your
298
+ normal permission rules, and nobody is there to answer an ask. Its prompt says the limits of any
299
+ earlier guarded turn in that session no longer apply. The next wake that is not trusted makes that
300
+ peer session guarded again, and its prompt says the limits hold now. The plugin keeps no trust of its own: after OpenCode
301
+ reloads it, every peer session is guarded until its next trusted wake. No tool, agent or teammate
302
+ can trust anyone. Trust only someone whose machine you would let run commands in this project.
303
+ The 0.1 option `inbound.trusted` is gone; a configuration that still sets it is refused.
304
+
305
+ ## Messaging by Jelly
306
+
307
+ The managed deployment is a directory realm (spec §15.8) whose identity this release pins
308
+ (`MESSAGING_BY_JELLY` in `src/domain/managed.ts`); see
309
+ [security.md](security.md#messaging-by-jelly-the-pinned-realm) for why and how to check it. There is
310
+ no plugin option for it: the realm is not configurable, and the plugin never writes a realm except
311
+ from the `/messaging` console.
312
+
313
+ **In the console.** A home is *fresh* when it has no `identity.json`, its `config.json` (if any)
314
+ names no `realm`, `relay` or `person`, and it has no `pairing.json`. `/messaging` on a fresh home
315
+ asks first: "Use Messaging by Jelly", then "Create or join a team (self-host)". Picking Messaging by
316
+ Jelly and confirming:
317
+
318
+ 1. reads `https://msg.toqanjelly.ai/.well-known/messaging.json` (5 s, no redirects). A `200` whose
319
+ `kind`, `url`, `issuer`, `resource`, `key` or `realm` differs from the pinned realm (or whose
320
+ `url`, `issuer` and `key` give another realm id) is refused: nothing is written and the daemon is
321
+ not started. Its `orgDomains` may differ; the pinned default is written. No answer, a redirect or
322
+ another status leaves the pinned realm to stand on its own;
323
+ 2. adds `realm` to `config.json`, keeping its other fields (the home is created 0700 and the file
324
+ 0600 if they do not exist). It checks the home is still fresh just before the file is renamed into
325
+ place, so a realm, a team or an identity written meanwhile is never overwritten;
326
+ 3. starts the daemon (when `daemon.autostart` allows), which makes this device's keys once and
327
+ reports `realm: "directory"`, `binding: "none"`;
328
+ 4. opens Devices, where **Link this device** makes this computer one of your devices once a device
329
+ that already has Jelly approves it.
330
+
331
+ `/messaging-status` and This device then read `Realm: Messaging by Jelly (directory)`, with the
332
+ binding; while the daemon is stopped, the realm line still shows from `config.json`.
333
+
334
+ **By hand, without the TUI.** Put this in `~/.messaging/config.json` (or `$MESSAGING_HOME/config.json`)
335
+ **before the daemon first starts in that home**, in a home that has no `identity.json`, no
336
+ `pairing.json` and no `relay` or `person` in its config:
337
+
338
+ ```json
339
+ {
340
+ "realm": {
341
+ "kind": "directory",
342
+ "url": "wss://msg.toqanjelly.ai/v1/connect",
343
+ "issuer": "https://id.toqanjelly.ai",
344
+ "resource": "https://msg.toqanjelly.ai",
345
+ "key": "Qp_RqcZhpEjtYqI8ybcUMo07ubT9bBb-8JnXpjnledg",
346
+ "orgDomains": ["prosus.com"]
347
+ }
348
+ }
349
+ ```
350
+
351
+ Then `messaging daemon start`, and link the device from the TUI (Devices, Link this device) or with
352
+ `messaging devices link`. Check the key out of band first
353
+ ([security.md](security.md#checking-the-key-out-of-band)). A home's realm never changes: to leave it,
354
+ move the home aside and start again. A `messaging` CLI command that writes this block is a later item.
355
+
356
+ ## Permission rules
357
+
358
+ Messaging tools use two OpenCode permission actions: `messaging.send` and `messaging.read`. Configure them
359
+ with normal `permissions` rules. [security.md](security.md) gives the resource formats and the
360
+ default behaviour. A line to a person asserts `messaging.send` on `<person>` and `<person>/` (so a
361
+ deny on `<person>/*` covers it), a post on `#<channel>`, and `read` and `search` assert
362
+ `messaging.read` on the conversation (`person:<person>`, `agent:<person>/<agent>` or
363
+ `channel:<name>`). In the example below, the `*` deny also blocks every line and post, because no
364
+ rule allows a person or a channel.
365
+
366
+ The plugin also adds defaults for the agent's own tools. `shell` asks before running any
367
+ `messaging` or `messagingd` command, and any command that names the Messaging home or its `identity.json`
368
+ and `messaging.db` files. `read`, `edit` and `external_directory` are denied under the Messaging home.
369
+ These defaults go after every broad allow (`*`, or an action allowed on `*`), so they hold for
370
+ agents without a base rule too; your specific rules come after them and win. See
371
+ [security.md](security.md#the-agents-shell-and-the-messaging-home).
372
+
373
+ ```jsonc
374
+ {
375
+ "permissions": [
376
+ // Only alice's agents: deny everything, then allow alice (the last matching rule wins).
377
+ { "action": "messaging.send", "resource": "*", "effect": "deny" },
378
+ { "action": "messaging.send", "resource": "alice/*", "effect": "allow" },
379
+ // Hide the contacts tool's results.
380
+ { "action": "messaging.read", "resource": "contacts", "effect": "deny" },
381
+ // Let the agent read the person's DM lines with bob without asking; keep it out of the hr channel.
382
+ { "action": "messaging.read", "resource": "person:bob", "effect": "allow" },
383
+ { "action": "messaging.read", "resource": "channel:hr", "effect": "deny" },
384
+ // Let the shell list the inbox without asking (the plugin's default asks for every messaging command).
385
+ { "action": "shell", "resource": "messaging inbox*", "effect": "allow" },
386
+ ],
387
+ }
388
+ ```
389
+
390
+ ## Debugging
391
+
392
+ Set `MESSAGING_PLUGIN_DEBUG=1` in the OpenCode server's environment. The plugin then writes event
393
+ names and error codes to stderr: registration (`no_identity` before a team), reconnects,
394
+ autostart, acks. It never writes
395
+ message text, names, ids, or tokens. When the server runs inside the TUI process, stderr paints over
396
+ the terminal UI, so set it only for `opencode serve` or the host suite.