@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/CHANGELOG.md +43 -0
- package/README.md +64 -4
- package/assets/icon.svg +1 -0
- package/dist/index.js +547 -129
- package/dist/messaging.js +271 -98
- package/dist/messagingd.js +887 -71
- package/dist/tui.js +196 -103
- package/docs/architecture.md +29 -0
- package/docs/configuration.md +7 -1
- package/docs/security.md +25 -0
- package/package.json +1251 -8
- package/views/section.html +299 -0
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
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
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
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
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("
|
|
6397
|
+
t: literal("wake.arm"),
|
|
6372
6398
|
req: Req,
|
|
6373
|
-
|
|
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("
|
|
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:
|
|
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:
|
|
6627
|
+
params: DeviceApproveParams.partial({ sas: true }),
|
|
6596
6628
|
result: LinkPreview
|
|
6597
6629
|
},
|
|
6598
6630
|
"device.approve": {
|
|
6599
|
-
params:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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: "
|
|
9993
|
+
sha256: "5cb51ae058c4b83cd18a56bc6c9c5a54505ee952a6cab72c5876ed570ed3e863"
|
|
9901
9994
|
},
|
|
9902
9995
|
"darwin-x64": {
|
|
9903
9996
|
os: "darwin",
|
|
9904
9997
|
cpu: "x64",
|
|
9905
9998
|
file: "bin/messagingd",
|
|
9906
|
-
sha256: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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.
|
|
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 {
|
package/docs/architecture.md
CHANGED
|
@@ -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`
|
package/docs/configuration.md
CHANGED
|
@@ -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.
|
|
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,
|