@joe-broadhead/opencode-messaging 0.0.0-stage → 0.4.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.
- package/CHANGELOG.md +129 -0
- package/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +281 -2
- package/dist/index.js +13699 -0
- package/dist/messaging.js +14510 -0
- package/dist/messagingd.js +27961 -0
- package/dist/rpc.js +5047 -0
- package/dist/third-party-notices.md +81 -0
- package/dist/tui.js +13805 -0
- package/docs/architecture.md +403 -0
- package/docs/configuration.md +397 -0
- package/docs/security.md +587 -0
- package/package.json +97 -4
- package/skills/opencode-messaging/SKILL.md +206 -0
|
@@ -0,0 +1,397 @@
|
|
|
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.4.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 **Sign in with Jelly** (in the browser at Jelly ID) or **Link this device**
|
|
329
|
+
(approved from a device that already has Jelly) makes this computer one of your devices.
|
|
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 sign in (`messaging login`, or the TUI's Devices, Sign in with
|
|
352
|
+
Jelly), or link the device from the TUI (Devices, Link this device) or with
|
|
353
|
+
`messaging devices link`. Check the key out of band first
|
|
354
|
+
([security.md](security.md#checking-the-key-out-of-band)). A home's realm never changes: to leave it,
|
|
355
|
+
move the home aside and start again. A `messaging` CLI command that writes this block is a later item.
|
|
356
|
+
|
|
357
|
+
## Permission rules
|
|
358
|
+
|
|
359
|
+
Messaging tools use two OpenCode permission actions: `messaging.send` and `messaging.read`. Configure them
|
|
360
|
+
with normal `permissions` rules. [security.md](security.md) gives the resource formats and the
|
|
361
|
+
default behaviour. A line to a person asserts `messaging.send` on `<person>` and `<person>/` (so a
|
|
362
|
+
deny on `<person>/*` covers it), a post on `#<channel>`, and `read` and `search` assert
|
|
363
|
+
`messaging.read` on the conversation (`person:<person>`, `agent:<person>/<agent>` or
|
|
364
|
+
`channel:<name>`). In the example below, the `*` deny also blocks every line and post, because no
|
|
365
|
+
rule allows a person or a channel.
|
|
366
|
+
|
|
367
|
+
The plugin also adds defaults for the agent's own tools. `shell` asks before running any
|
|
368
|
+
`messaging` or `messagingd` command, and any command that names the Messaging home or its `identity.json`
|
|
369
|
+
and `messaging.db` files. `read`, `edit` and `external_directory` are denied under the Messaging home.
|
|
370
|
+
These defaults go after every broad allow (`*`, or an action allowed on `*`), so they hold for
|
|
371
|
+
agents without a base rule too; your specific rules come after them and win. See
|
|
372
|
+
[security.md](security.md#the-agents-shell-and-the-messaging-home).
|
|
373
|
+
|
|
374
|
+
```jsonc
|
|
375
|
+
{
|
|
376
|
+
"permissions": [
|
|
377
|
+
// Only alice's agents: deny everything, then allow alice (the last matching rule wins).
|
|
378
|
+
{ "action": "messaging.send", "resource": "*", "effect": "deny" },
|
|
379
|
+
{ "action": "messaging.send", "resource": "alice/*", "effect": "allow" },
|
|
380
|
+
// Hide the contacts tool's results.
|
|
381
|
+
{ "action": "messaging.read", "resource": "contacts", "effect": "deny" },
|
|
382
|
+
// Let the agent read the person's DM lines with bob without asking; keep it out of the hr channel.
|
|
383
|
+
{ "action": "messaging.read", "resource": "person:bob", "effect": "allow" },
|
|
384
|
+
{ "action": "messaging.read", "resource": "channel:hr", "effect": "deny" },
|
|
385
|
+
// Let the shell list the inbox without asking (the plugin's default asks for every messaging command).
|
|
386
|
+
{ "action": "shell", "resource": "messaging inbox*", "effect": "allow" },
|
|
387
|
+
],
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Debugging
|
|
392
|
+
|
|
393
|
+
Set `MESSAGING_PLUGIN_DEBUG=1` in the OpenCode server's environment. The plugin then writes event
|
|
394
|
+
names and error codes to stderr: registration (`no_identity` before a team), reconnects,
|
|
395
|
+
autostart, acks. It never writes
|
|
396
|
+
message text, names, ids, or tokens. When the server runs inside the TUI process, stderr paints over
|
|
397
|
+
the terminal UI, so set it only for `opencode serve` or the host suite.
|