@joe-broadhead/opencode-messaging 0.4.0 → 0.5.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/dist/tui.js CHANGED
@@ -5779,7 +5779,8 @@ looseObject({
5779
5779
  jti: string$1().regex(/^[\x21-\x7e]{1,128}$/, "printable jti"),
5780
5780
  iss: string$1().max(2048).optional(),
5781
5781
  dev: string$1().max(200).optional(),
5782
- cls: string$1().max(32).optional()
5782
+ cls: string$1().max(32).optional(),
5783
+ mbr: string$1().max(200).optional()
5783
5784
  });
5784
5785
  looseObject({
5785
5786
  aud: string$1().min(1).max(2048),
@@ -6305,78 +6306,104 @@ const Size = number$1().int().positive();
6305
6306
  const Handle = string$1().regex(/^[a-z][a-z0-9-]{0,31}$/, "lowercase handle");
6306
6307
  const Email = string$1().max(254).regex(/^[^\s@A-Z]{1,64}@(?:[a-z0-9-]+\.)+[a-z0-9-]+$/, "lower-case email address");
6307
6308
  const OrgId = string$1().min(1).max(200);
6308
- discriminatedUnion("t", [
6309
- object({
6310
- t: literal("dir.bind"),
6311
- req: Req,
6312
- entry: LogEntry
6313
- }),
6314
- object({
6315
- t: literal("hello"),
6316
- realm: RealmId,
6309
+ const DirBindFrame = object({
6310
+ t: literal("dir.bind"),
6311
+ req: Req,
6312
+ entry: LogEntry
6313
+ });
6314
+ const HostedHelloFrame = object({
6315
+ t: literal("hello"),
6316
+ realm: RealmId,
6317
+ person: HostedPersonId,
6318
+ device: DeviceId,
6319
+ ts: Millis,
6320
+ sig: Base64Url
6321
+ });
6322
+ const DirAppendFrame = object({
6323
+ t: literal("dir.append"),
6324
+ req: Req,
6325
+ entry: LogEntry
6326
+ });
6327
+ const DirKeysFrame = object({
6328
+ t: literal("dir.keys"),
6329
+ req: Req,
6330
+ stmt: KeysStatement
6331
+ });
6332
+ const DirSthFrame = object({
6333
+ t: literal("dir.sth"),
6334
+ req: Req,
6335
+ size: Size.optional(),
6336
+ from: Size.optional(),
6337
+ epoch: number$1().int().nonnegative().optional()
6338
+ });
6339
+ const DirLookupFrame = object({
6340
+ t: literal("dir.lookup"),
6341
+ req: Req,
6342
+ persons: array(object({
6317
6343
  person: HostedPersonId,
6318
- device: DeviceId,
6319
- ts: Millis,
6320
- sig: Base64Url
6321
- }),
6322
- object({
6323
- t: literal("dir.append"),
6324
- req: Req,
6325
- entry: LogEntry
6326
- }),
6327
- object({
6328
- t: literal("dir.keys"),
6329
- req: Req,
6330
- stmt: KeysStatement
6331
- }),
6332
- object({
6333
- t: literal("dir.sth"),
6334
- req: Req,
6335
- size: Size.optional(),
6336
- from: Size.optional(),
6337
- epoch: number$1().int().nonnegative().optional()
6338
- }),
6339
- object({
6340
- t: literal("dir.lookup"),
6341
- req: Req,
6342
- persons: array(object({
6343
- person: HostedPersonId,
6344
- knownSeq: number$1().int().nonnegative().optional()
6345
- })).min(1).max(LIMITS.dirLookupPersons),
6346
- size: Size.optional()
6347
- }),
6348
- object({
6349
- t: literal("dir.device"),
6350
- req: Req,
6351
- device: DeviceId
6352
- }),
6353
- object({
6354
- t: literal("dir.resolve"),
6355
- req: Req,
6356
- handle: Handle.optional(),
6357
- email: Email.optional()
6358
- }).refine((frame) => frame.handle === void 0 !== (frame.email === void 0), { message: "exactly one of handle and email" }),
6359
- object({
6360
- t: literal("dir.search"),
6361
- req: Req,
6362
- query: string$1().min(1).max(64),
6363
- limit: number$1().int().min(1).max(LIMITS.dirSearchResults)
6364
- }),
6365
- object({
6366
- t: literal("dir.watch"),
6367
- req: Req,
6368
- persons: array(HostedPersonId).max(LIMITS.watchPersons)
6369
- }),
6344
+ knownSeq: number$1().int().nonnegative().optional()
6345
+ })).min(1).max(LIMITS.dirLookupPersons),
6346
+ size: Size.optional()
6347
+ });
6348
+ const DirDeviceFrame = object({
6349
+ t: literal("dir.device"),
6350
+ req: Req,
6351
+ device: DeviceId
6352
+ });
6353
+ const DirResolveFrame = object({
6354
+ t: literal("dir.resolve"),
6355
+ req: Req,
6356
+ handle: Handle.optional(),
6357
+ email: Email.optional()
6358
+ }).refine((frame) => frame.handle === void 0 !== (frame.email === void 0), { message: "exactly one of handle and email" });
6359
+ const DirSearchFrame = object({
6360
+ t: literal("dir.search"),
6361
+ req: Req,
6362
+ query: string$1().min(1).max(64),
6363
+ limit: number$1().int().min(1).max(LIMITS.dirSearchResults)
6364
+ });
6365
+ const DirWatchFrame = object({
6366
+ t: literal("dir.watch"),
6367
+ req: Req,
6368
+ persons: array(HostedPersonId).max(LIMITS.watchPersons)
6369
+ });
6370
+ const PresenceWatchFrame = object({
6371
+ t: literal("presence.watch"),
6372
+ req: Req,
6373
+ persons: array(HostedPersonId).max(LIMITS.watchPersons)
6374
+ });
6375
+ const DirEraseFrame = object({
6376
+ t: literal("dir.erase"),
6377
+ req: Req,
6378
+ entries: array(LogEntry).min(1).max(LIMITS.devicesPerPerson),
6379
+ token: Jws
6380
+ });
6381
+ const WAKE_TICKET_BYTES = 4096;
6382
+ const WakeUrl = string$1().max(2048).regex(/^https?:\/\/[^/?#@\s]+\/hosts\/hst_[A-Za-z0-9._:-]{1,123}\/wake$/, "a Jelly ID wake URL");
6383
+ discriminatedUnion("t", [
6384
+ DirBindFrame,
6385
+ HostedHelloFrame,
6386
+ DirAppendFrame,
6387
+ DirKeysFrame,
6388
+ DirSthFrame,
6389
+ DirLookupFrame,
6390
+ DirDeviceFrame,
6391
+ DirResolveFrame,
6392
+ DirSearchFrame,
6393
+ DirWatchFrame,
6394
+ PresenceWatchFrame,
6395
+ DirEraseFrame,
6370
6396
  object({
6371
- t: literal("presence.watch"),
6397
+ t: literal("wake.arm"),
6372
6398
  req: Req,
6373
- persons: array(HostedPersonId).max(LIMITS.watchPersons)
6399
+ url: WakeUrl,
6400
+ ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
6401
+ agents: array(SessionName).max(64),
6402
+ awaiting: array(Ulid).max(500)
6374
6403
  }),
6375
6404
  object({
6376
- t: literal("dir.erase"),
6377
- req: Req,
6378
- entries: array(LogEntry).min(1).max(LIMITS.devicesPerPerson),
6379
- token: Jws
6405
+ t: literal("wake.disarm"),
6406
+ req: Req
6380
6407
  })
6381
6408
  ]);
6382
6409
  const HostedWelcomeFrame = object({
@@ -6525,6 +6552,19 @@ const LoginState = discriminatedUnion("state", [
6525
6552
  purpose: LoginPurpose
6526
6553
  })
6527
6554
  ]);
6555
+ const DeviceRemoveParams = strictObject({
6556
+ device: MachineId,
6557
+ reason: _enum([
6558
+ "self",
6559
+ "removed",
6560
+ "lost"
6561
+ ]).optional(),
6562
+ token: Jws.optional()
6563
+ });
6564
+ const DeviceApproveParams = strictObject({
6565
+ link: LinkText,
6566
+ sas: Sas
6567
+ });
6528
6568
  const DEVICE_METHODS = {
6529
6569
  "device.status": {
6530
6570
  params: strictObject({}),
@@ -6547,15 +6587,7 @@ const DEVICE_METHODS = {
6547
6587
  result: object({ devices: array(DeviceListEntry) })
6548
6588
  },
6549
6589
  "device.remove": {
6550
- params: strictObject({
6551
- device: MachineId,
6552
- reason: _enum([
6553
- "self",
6554
- "removed",
6555
- "lost"
6556
- ]).optional(),
6557
- token: Jws.optional()
6558
- }),
6590
+ params: DeviceRemoveParams,
6559
6591
  result: object({ seq: number$1().int().nonnegative() })
6560
6592
  },
6561
6593
  "device.link": {
@@ -6592,14 +6624,11 @@ const DEVICE_METHODS = {
6592
6624
  result: object({ ok: literal(true) })
6593
6625
  },
6594
6626
  "device.approve.preview": {
6595
- params: strictObject({ link: LinkText }),
6627
+ params: DeviceApproveParams.partial({ sas: true }),
6596
6628
  result: LinkPreview
6597
6629
  },
6598
6630
  "device.approve": {
6599
- params: strictObject({
6600
- link: LinkText,
6601
- sas: Sas
6602
- }),
6631
+ params: DeviceApproveParams,
6603
6632
  result: object({
6604
6633
  device: MachineId,
6605
6634
  seq: number$1().int().nonnegative()
@@ -6824,6 +6853,16 @@ const PersonReach = object({
6824
6853
  source: _enum(["card", "default"]),
6825
6854
  at: Millis.optional()
6826
6855
  });
6856
+ const RequestAnswerParams = strictObject({
6857
+ id: Ulid,
6858
+ answer: RequestAnswer,
6859
+ all: boolean().optional()
6860
+ });
6861
+ const ContactRequestParams = strictObject({
6862
+ email: Email,
6863
+ text: RequestText,
6864
+ suggestion: Ulid.optional()
6865
+ });
6827
6866
  const CONSENT_METHODS = {
6828
6867
  "grant.list": {
6829
6868
  params: strictObject({ subject: GrantTarget.optional() }),
@@ -6845,19 +6884,35 @@ const CONSENT_METHODS = {
6845
6884
  result: object({ requests: array(HeldRequest).max(LIMITS.heldTotal * 3) })
6846
6885
  },
6847
6886
  "request.answer": {
6848
- params: strictObject({
6849
- id: Ulid,
6850
- answer: RequestAnswer,
6851
- all: boolean().optional()
6852
- }),
6887
+ params: RequestAnswerParams,
6853
6888
  result: object({ answered: array(Ulid).max(LIMITS.heldTotal) })
6854
6889
  },
6890
+ "request.answer.preview": {
6891
+ params: RequestAnswerParams,
6892
+ result: object({
6893
+ kind: _enum([
6894
+ "message",
6895
+ "task",
6896
+ "contact",
6897
+ "suggestion"
6898
+ ]),
6899
+ from: object({
6900
+ name: string$1().max(200),
6901
+ email: string$1().max(320).optional(),
6902
+ org: _enum([
6903
+ "same",
6904
+ "other",
6905
+ "none"
6906
+ ])
6907
+ }),
6908
+ agent: SessionName.optional(),
6909
+ to: SessionName.optional(),
6910
+ count: number$1().int().positive(),
6911
+ sentence: string$1().max(500)
6912
+ })
6913
+ },
6855
6914
  "contact.request": {
6856
- params: strictObject({
6857
- email: Email,
6858
- text: RequestText,
6859
- suggestion: Ulid.optional()
6860
- }),
6915
+ params: ContactRequestParams,
6861
6916
  result: object({
6862
6917
  id: Ulid,
6863
6918
  conversation: RoomRef,
@@ -6865,7 +6920,7 @@ const CONSENT_METHODS = {
6865
6920
  })
6866
6921
  },
6867
6922
  "contact.request.preview": {
6868
- params: strictObject({ email: Email }),
6923
+ params: ContactRequestParams.partial({ text: true }),
6869
6924
  result: object({ found: boolean() })
6870
6925
  },
6871
6926
  "card.get": {
@@ -7033,7 +7088,8 @@ const SURFACE_METHODS = {
7033
7088
  result: PersonSummary.extend({ devices: array(object({
7034
7089
  device: MachineId,
7035
7090
  label: MachineLabel,
7036
- self: boolean()
7091
+ self: boolean(),
7092
+ away: literal(true).optional()
7037
7093
  })) })
7038
7094
  }
7039
7095
  };
@@ -7151,8 +7207,45 @@ const JELLY_METHODS = {
7151
7207
  params: strictObject({ ids: array(NoticeId).min(1).max(200) }),
7152
7208
  result: object({ ok: literal(true) })
7153
7209
  },
7210
+ "daemon.woke": {
7211
+ params: strictObject({}),
7212
+ result: object({ ok: literal(true) })
7213
+ },
7214
+ "person.attached": {
7215
+ params: strictObject({ attached: boolean() }),
7216
+ result: object({ ok: literal(true) })
7217
+ },
7218
+ "wake.arm": {
7219
+ params: strictObject({
7220
+ url: WakeUrl,
7221
+ ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
7222
+ agents: array(SessionName).max(64)
7223
+ }),
7224
+ result: object({
7225
+ relay: _enum(["armed", "pending"]),
7226
+ awaiting: number$1().int().nonnegative()
7227
+ })
7228
+ },
7229
+ "wake.disarm": {
7230
+ params: strictObject({}),
7231
+ result: object({ relay: _enum(["disarmed", "pending"]) })
7232
+ },
7233
+ "daemon.snapshot": {
7234
+ params: strictObject({ done: literal(true).optional() }),
7235
+ result: union$1([object({
7236
+ folder: string$1().max(4096),
7237
+ files: array(object({
7238
+ name: string$1().max(64),
7239
+ bytes: number$1().int().nonnegative(),
7240
+ sha256: string$1().regex(/^[0-9a-f]{64}$/)
7241
+ })).max(8),
7242
+ storeSchema: number$1().int().positive(),
7243
+ at: Millis,
7244
+ until: Millis
7245
+ }), object({ released: boolean() })])
7246
+ },
7154
7247
  "device.remove.preview": {
7155
- params: strictObject({ device: MachineId }),
7248
+ params: DeviceRemoveParams,
7156
7249
  result: object({
7157
7250
  label: MachineLabel,
7158
7251
  kind: DeviceKind.optional(),
@@ -9756,7 +9849,7 @@ var MessagingClient = class MessagingClient {
9756
9849
  timer = setTimeout(() => reject(new DaemonMismatchError(`the process on the Messaging daemon socket did not say which version it is within ${wait} ms: it is a messagingd from before this version, or it is stuck. ${REMEDY}`)), wait);
9757
9850
  })]);
9758
9851
  if (pointer !== void 0 && hello.pid !== pointer.pid) throw new MessagingError("forbidden", "the process on the Messaging daemon's pipe is not the daemon that published it");
9759
- const expected = options.version ?? "0.4.0";
9852
+ const expected = options.version ?? "0.5.0";
9760
9853
  if (hello.protocol !== 1 || hello.version !== expected) throw new DaemonMismatchError(`the Messaging daemon for this home is version ${shown(hello.version)} (IPC protocol ${hello.protocol}), and this client is version ${shown(expected)} (IPC protocol 1). ${REMEDY}`, hello);
9761
9854
  client.daemonHello = hello;
9762
9855
  return client;
@@ -9891,39 +9984,39 @@ function toolError(code, message) {
9891
9984
  return new MessagingError(code, JSON.stringify(toolErrorBody(code, message.slice(0, MAX_MESSAGE))));
9892
9985
  }
9893
9986
  // build/domain/platforms.js
9894
- const DAEMON_VERSION = "0.4.0";
9987
+ const DAEMON_VERSION = "0.5.0";
9895
9988
  const DAEMON_TARGETS = {
9896
9989
  "darwin-arm64": {
9897
9990
  os: "darwin",
9898
9991
  cpu: "arm64",
9899
9992
  file: "bin/messagingd",
9900
- sha256: "2e9b9b4092507e00fa754b8f2d529c67597a53c74cba7bd8ded8405e235b5459"
9993
+ sha256: "5cb51ae058c4b83cd18a56bc6c9c5a54505ee952a6cab72c5876ed570ed3e863"
9901
9994
  },
9902
9995
  "darwin-x64": {
9903
9996
  os: "darwin",
9904
9997
  cpu: "x64",
9905
9998
  file: "bin/messagingd",
9906
- sha256: "586dae07a46eede9e75c3227f20f23c089d094878c542436aa43aef5420979be"
9999
+ sha256: "fd767f2fbe80c2140d12adb134b32f790a1f668acf11b4354a5e75dac14fe25c"
9907
10000
  },
9908
10001
  "linux-arm64-gnu": {
9909
10002
  os: "linux",
9910
10003
  cpu: "arm64",
9911
10004
  libc: "glibc",
9912
10005
  file: "bin/messagingd",
9913
- sha256: "217d97c06a6cfff017a32ddf3c5ac3f0c07020e4263266efc7a22e0dd61de694"
10006
+ sha256: "eddf27a9fdd55f1fbf62b5bdf6718c442790684a0f959f330ccfb03f68e91a02"
9914
10007
  },
9915
10008
  "linux-x64-gnu": {
9916
10009
  os: "linux",
9917
10010
  cpu: "x64",
9918
10011
  libc: "glibc",
9919
10012
  file: "bin/messagingd",
9920
- sha256: "8bf49cd428a461ceb01c87c0e7d49427331cac1773724e59bb164810267d9adb"
10013
+ sha256: "17c9fef1eef307ffca13a1151dd708eaf6f63dda4e448339ac991162abed1317"
9921
10014
  },
9922
10015
  "win32-x64": {
9923
10016
  os: "win32",
9924
10017
  cpu: "x64",
9925
10018
  file: "bin/messagingd.exe",
9926
- sha256: "eaa652a089683ba6bcce61b7bdcef2244051a7bea6bb167b488e5ce32b8825b3"
10019
+ sha256: "363da65dcea1fe161438b05660ca15c3c05655e7539ce4463797698a2cb3f844"
9927
10020
  }
9928
10021
  };
9929
10022
  function daemonTarget(platform, targets = DAEMON_TARGETS) {
@@ -10014,7 +10107,7 @@ async function packagedExecutable(target, entry, from) {
10014
10107
  } catch {
10015
10108
  throw refuse("has an unreadable package.json");
10016
10109
  }
10017
- if (manifest.name !== name || manifest.version !== "0.4.0") throw refuse(`is not version ${DAEMON_VERSION}`);
10110
+ if (manifest.name !== name || manifest.version !== "0.5.0") throw refuse(`is not version ${DAEMON_VERSION}`);
10018
10111
  const path = join(dirname(manifestPath), ...entry.file.split("/"));
10019
10112
  let stats;
10020
10113
  try {
@@ -32,6 +32,7 @@ src/
32
32
  connection.ts the daemon link: connect, autostart hook, reconnect, agent-method allowlist
33
33
  host/ everything that imports @opencode/plugin
34
34
  context.ts host types derived once
35
+ jelly.ts inside Jelly: the services accessor, turn requests to jelly.core, the record of Jelly turns
35
36
  tools.ts namespace + tool adapters: validate, local checks, assert, runtime, render
36
37
  permissions.ts assert request builders; default rules (send ask, shell asks, home denies, DM read ask) via an agent transform
37
38
  peers.ts peer sessions (find or create, stored rules, quarantine folder) and the guard's evaluate and context hooks
@@ -47,6 +48,7 @@ src/
47
48
  inbound.ts delivery decision, wake prompts, notices, task notes, the mid-turn note
48
49
  peers.ts peer-turn policy: the guard's verdicts, stored rules, tool allowlist, session marker
49
50
  setup.ts the setup note for a machine that has not joined a team
51
+ jelly.ts inside Jelly: the accessor entry, the shared session key, turn profile, key, prompt and answers
50
52
  managed.ts Messaging by Jelly: the pinned realm, and the check of its messaging.json
51
53
  budget.ts per-peer wake budget (sliding window)
52
54
  naming.ts keys, session names, addresses, ids
@@ -72,6 +74,9 @@ src/
72
74
  status.ts This device and /messaging-status
73
75
  qr.ts terminal QR codes
74
76
  skills/opencode-messaging/SKILL.md
77
+ assets/icon.svg the plugin's mark in Jelly
78
+ views/section.html Jelly's section page (a sealed page: People, Chats, Requests, Devices, Your agents)
79
+ scripts/jelly-declaration.mjs builds package.json `jelly` from the vendored daemon's IPC schemas
75
80
  ```
76
81
 
77
82
  | Layer | May import |
@@ -127,6 +132,30 @@ the socket protocol.
127
132
  - `Link.call` refuses any method not in `AGENT_METHODS`. It turns `MessagingError` and JSON-RPC
128
133
  errors into code-prefixed tool errors (`daemon_unavailable: …`).
129
134
 
135
+ ## Inside Jelly
136
+
137
+ `src/index.ts` reads `globalThis[Symbol.for("jelly.services")]` once at setup (`host/jelly.ts`). Its
138
+ presence is Jelly mode, and changes four things; without it nothing changes.
139
+
140
+ - **The link.** `DaemonConnection` gets an `endpoint` (the accessor's socket, asked on every
141
+ connect) and no `start`: nothing ever spawns a daemon, and no home is resolved. `Runtime.home` is
142
+ the socket's folder, for the permission defaults only.
143
+ - **Registration.** `Sessions` gives every OpenCode session of the engine one key, `jelly:<name>`,
144
+ and registers it once per connection with the accessor's `name` and `ticket` and no `host`; a
145
+ second session of the agent reuses that registration. `ensure` skips the `identity.json` check: the
146
+ daemon answers for itself. A `ticket_invalid` on `session.register` re-reads the accessor and
147
+ retries only with a different ticket. On any other call (the ticket ended; the daemon keeps the
148
+ session), `DaemonConnection` runs its `renew` hook, `Sessions.renew`: register the same key with the
149
+ accessor's ticket on that connection, then make the call once more. Busy/idle is the agent's: busy while any of its sessions is.
150
+ - **Wakes.** Delivery decides a wake as if the working session were idle (a turn runs in its own
151
+ conversation), keeps the coalescing window and the per-person budget, and, when the window
152
+ closes, files one `turn` request per sender person through `HostBridge.turn` (`jelly.core`
153
+ `request` `{ id, kind: "turn", body: { package, key, profile, prompt }, keep: true }`). A started
154
+ turn acks the items `handed_to_agent`; any refusal posts notes instead. The setup note is off.
155
+ - **The guard.** `registerJellyGuard` replaces the peer guard: only `messaging.*` checks, only in a
156
+ session recorded as a guarded Jelly turn (or marked `jelly.peer` with no record, which is
157
+ refused). No session rules, no tool removal. The `config` RPC refuses, so no console runs.
158
+
130
159
  ## The `/messaging` console
131
160
 
132
161
  The TUI part is a separate entry (`./tui`) with its own connection, `ConsoleLink`
@@ -51,6 +51,12 @@ where a console started a daemon in `~/.messaging` while the configured home was
51
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
52
  | `inbound.wakeBudget.windowMs` | integer, 60000–86400000 | `3600000` (1 h) | The sliding window of the wake budget, in milliseconds. |
53
53
 
54
+ **Inside Jelly** (Jelly's services accessor is present; [README](../README.md#running-inside-jelly)),
55
+ `home`, `daemon.autostart`, `daemon.bin`, `daemon.node` and `session.name` do nothing: the socket,
56
+ the session name and its ticket come from Jelly, and Jelly's host runs the daemon. The `inbound`
57
+ options still apply, with `inbound.wake` deciding whether a wake files a turn with Jelly, and the
58
+ wake budget bounding those requests.
59
+
54
60
  ### `home`
55
61
 
56
62
  The plugin uses the first one that is set: the `home` option, then the `MESSAGING_HOME` environment
@@ -83,7 +89,7 @@ The plugin picks the first that applies (`src/process/daemon.ts`, the one place
83
89
  | Linux x64, glibc | `@joe-broadhead/messaging-bin-linux-x64-gnu` | `bin/messagingd` |
84
90
  | Windows x64 | `@joe-broadhead/messaging-bin-win32-x64` | `bin/messagingd.exe` (not code-signed; see [Windows](#windows)) |
85
91
 
86
- Each is pinned at exactly the messaging release the plugin is built against (0.4.0), and npm
92
+ Each is pinned at exactly the messaging release the plugin is built against (0.5.0), and npm
87
93
  installs only the one whose `os` and `cpu` match. The plugin finds it from its own install
88
94
  location the way Node resolves a package, and admits it only when the package is that version,
89
95
  the executable is a regular file (not a link) with owner read and execute and no permission
package/docs/security.md CHANGED
@@ -322,6 +322,31 @@ that they hold even if an earlier turn ran without them; a trusted turn names th
322
322
  they no longer apply. The guard itself never depends on the model reading this: it is enforced per
323
323
  check and per request.
324
324
 
325
+ ### Inside Jelly
326
+
327
+ In a Jelly engine (Jelly's services accessor is present; [README](../README.md#running-inside-jelly)),
328
+ the plugin creates no peer sessions and stores no session rules. A teammate's wake is a `turn`
329
+ request to Jelly's host, which starts it in the conversation's own engine session and marks that
330
+ session `jelly.peer` by the profile the plugin chose from the daemon's record (`guarded`, or
331
+ `trusted` only when the daemon called every item trusted; never from the peer's text). Jelly's core
332
+ plugin then denies, for a guarded turn, a shell, edits outside the conversation's folder, outward
333
+ tools and changes to sign-ins, permissions or plugins; for a trusted turn, only those changes.
334
+
335
+ The plugin keeps one layer of its own there: a `permission.evaluate` hook that applies the
336
+ Messaging rows of the table above (`messaging.send` only back to the turn's person, `messaging.read`
337
+ only of that person, nothing that would ask), and Messaging tool results scoped to that person. It
338
+ records which engine session each started turn runs in, in plugin storage, so a reload keeps the
339
+ limit. A session Jelly marked that the plugin has no record of may not read or send at all, and a
340
+ session the host cannot look up is refused (fail closed). Every other action is Jelly's fence to
341
+ decide; the plugin leaves it alone so the two fences never disagree about, say, the conversation's
342
+ folder.
343
+
344
+ **The session ticket.** On a Jelly host every engine can open the service's agent socket, so a
345
+ name alone would let one agent take over another's inbox. The plugin registers only the name
346
+ Jelly's ticket names, with the ticket; the daemon verifies it against the host's action key and
347
+ refuses any other name. The ticket is never logged, and it is read from the accessor at each
348
+ registration, never stored.
349
+
325
350
  ## Untrusted content framing
326
351
 
327
352
  Everything a peer authored reaches the model inside a fence. That covers message parts,