@ccmsg/protocol 1.22.1 → 1.23.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/package.json +1 -1
- package/src/attributes.ts +56 -56
- package/src/common/auth.ts +4 -4
- package/src/common/ping.ts +2 -2
- package/src/common/topics.ts +6 -0
- package/src/control/dump.ts +22 -1
- package/src/control/session.ts +12 -4
- package/src/envelope.ts +1 -1
- package/src/errors.ts +3 -3
- package/src/fixtures/messaging.ts +1 -0
- package/src/fixtures/topics.ts +21 -1
- package/src/identifiers.ts +1 -1
- package/src/messaging/message.ts +65 -4
- package/src/messaging/notify.ts +12 -1
package/package.json
CHANGED
package/src/attributes.ts
CHANGED
|
@@ -8,10 +8,10 @@ import type { Capability, Role } from "./identifiers.ts";
|
|
|
8
8
|
* different op. */
|
|
9
9
|
export type Plane = "common" | "messaging" | "control" | "mesh";
|
|
10
10
|
|
|
11
|
-
/** `
|
|
11
|
+
/** `owner_instance` ops answer for one instance's processes, paths and
|
|
12
12
|
* handles, so they are forwarded to the instance that owns the subject.
|
|
13
|
-
* `
|
|
14
|
-
export type Locality = "
|
|
13
|
+
* `any_instance` ops are answerable by whichever instance is asked. */
|
|
14
|
+
export type Locality = "owner_instance" | "any_instance";
|
|
15
15
|
|
|
16
16
|
export interface OpAttributes {
|
|
17
17
|
readonly plane: Plane;
|
|
@@ -65,47 +65,47 @@ export const OP_ATTRIBUTES = {
|
|
|
65
65
|
//
|
|
66
66
|
// The greetings and `instance.ping` address the instance the caller reached, so
|
|
67
67
|
// there is nothing to forward and no unreachable instance to report — which
|
|
68
|
-
// is why they are `
|
|
68
|
+
// is why they are `any_instance` despite answering about one instance.
|
|
69
69
|
"hello.session": {
|
|
70
70
|
plane: "common",
|
|
71
71
|
roles: ALL_ROLES,
|
|
72
72
|
needs_hello: false,
|
|
73
|
-
locality: "
|
|
73
|
+
locality: "any_instance",
|
|
74
74
|
errors: [],
|
|
75
75
|
},
|
|
76
76
|
"hello.user": {
|
|
77
77
|
plane: "common",
|
|
78
78
|
roles: ALL_ROLES,
|
|
79
79
|
needs_hello: false,
|
|
80
|
-
locality: "
|
|
80
|
+
locality: "any_instance",
|
|
81
81
|
errors: [],
|
|
82
82
|
},
|
|
83
83
|
"hello.instance": {
|
|
84
84
|
plane: "common",
|
|
85
85
|
roles: ALL_ROLES,
|
|
86
86
|
needs_hello: false,
|
|
87
|
-
locality: "
|
|
87
|
+
locality: "any_instance",
|
|
88
88
|
errors: [],
|
|
89
89
|
},
|
|
90
90
|
"instance.ping": {
|
|
91
91
|
plane: "common",
|
|
92
92
|
roles: ALL_ROLES,
|
|
93
93
|
needs_hello: true,
|
|
94
|
-
locality: "
|
|
94
|
+
locality: "any_instance",
|
|
95
95
|
errors: [],
|
|
96
96
|
},
|
|
97
97
|
"instance.shutdown": {
|
|
98
98
|
plane: "common",
|
|
99
99
|
roles: USER_ONLY,
|
|
100
100
|
needs_hello: true,
|
|
101
|
-
locality: "
|
|
101
|
+
locality: "owner_instance",
|
|
102
102
|
errors: [],
|
|
103
103
|
},
|
|
104
104
|
"session.stopping": {
|
|
105
105
|
plane: "common",
|
|
106
106
|
roles: SESSION_ONLY,
|
|
107
107
|
needs_hello: true,
|
|
108
|
-
locality: "
|
|
108
|
+
locality: "owner_instance",
|
|
109
109
|
errors: [],
|
|
110
110
|
},
|
|
111
111
|
// Open to every role, with which role may have which topic left to the topic
|
|
@@ -115,20 +115,20 @@ export const OP_ATTRIBUTES = {
|
|
|
115
115
|
plane: "common",
|
|
116
116
|
roles: ALL_ROLES,
|
|
117
117
|
needs_hello: true,
|
|
118
|
-
locality: "
|
|
118
|
+
locality: "any_instance",
|
|
119
119
|
errors: ["topic_unknown"],
|
|
120
120
|
},
|
|
121
121
|
"topic.unsubscribe": {
|
|
122
122
|
plane: "common",
|
|
123
123
|
roles: ALL_ROLES,
|
|
124
124
|
needs_hello: true,
|
|
125
|
-
locality: "
|
|
125
|
+
locality: "any_instance",
|
|
126
126
|
errors: ["topic_unknown"],
|
|
127
127
|
},
|
|
128
128
|
|
|
129
129
|
// The four ops that authenticate a person are open to every role for the
|
|
130
130
|
// same reason the greetings are: they run before there is an identity to
|
|
131
|
-
// check, and what they answer is what settles one. They are `
|
|
131
|
+
// check, and what they answer is what settles one. They are `any_instance` because whichever
|
|
132
132
|
// instance is reached answers — behind a load balancer that is not a choice
|
|
133
133
|
// the caller makes — and each asks the issuing instance itself for the parts
|
|
134
134
|
// only it holds.
|
|
@@ -136,7 +136,7 @@ export const OP_ATTRIBUTES = {
|
|
|
136
136
|
plane: "common",
|
|
137
137
|
roles: ALL_ROLES,
|
|
138
138
|
needs_hello: false,
|
|
139
|
-
locality: "
|
|
139
|
+
locality: "any_instance",
|
|
140
140
|
carrier: "http",
|
|
141
141
|
errors: [],
|
|
142
142
|
},
|
|
@@ -144,7 +144,7 @@ export const OP_ATTRIBUTES = {
|
|
|
144
144
|
plane: "common",
|
|
145
145
|
roles: ALL_ROLES,
|
|
146
146
|
needs_hello: false,
|
|
147
|
-
locality: "
|
|
147
|
+
locality: "any_instance",
|
|
148
148
|
carrier: "http",
|
|
149
149
|
errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
|
|
150
150
|
},
|
|
@@ -152,7 +152,7 @@ export const OP_ATTRIBUTES = {
|
|
|
152
152
|
plane: "common",
|
|
153
153
|
roles: ALL_ROLES,
|
|
154
154
|
needs_hello: false,
|
|
155
|
-
locality: "
|
|
155
|
+
locality: "any_instance",
|
|
156
156
|
carrier: "http",
|
|
157
157
|
errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
|
|
158
158
|
},
|
|
@@ -160,7 +160,7 @@ export const OP_ATTRIBUTES = {
|
|
|
160
160
|
plane: "common",
|
|
161
161
|
roles: ALL_ROLES,
|
|
162
162
|
needs_hello: false,
|
|
163
|
-
locality: "
|
|
163
|
+
locality: "any_instance",
|
|
164
164
|
carrier: "http",
|
|
165
165
|
errors: ["auth_invalid", "auth_expired", "auth_unknown_issuer"],
|
|
166
166
|
},
|
|
@@ -170,24 +170,24 @@ export const OP_ATTRIBUTES = {
|
|
|
170
170
|
plane: "common",
|
|
171
171
|
roles: USER_ONLY,
|
|
172
172
|
needs_hello: true,
|
|
173
|
-
locality: "
|
|
173
|
+
locality: "any_instance",
|
|
174
174
|
errors: ["auth_invalid", "auth_expired"],
|
|
175
175
|
},
|
|
176
|
-
// Between instances: what an issuer alone can answer.
|
|
176
|
+
// Between instances: what an issuer alone can answer. `owner_instance` by the
|
|
177
177
|
// usual rule — the subject belongs to one instance, and it is reached by
|
|
178
178
|
// `to_instance` being that instance's id.
|
|
179
179
|
"auth.resolve": {
|
|
180
180
|
plane: "common",
|
|
181
181
|
roles: INSTANCE_ONLY,
|
|
182
182
|
needs_hello: true,
|
|
183
|
-
locality: "
|
|
183
|
+
locality: "owner_instance",
|
|
184
184
|
errors: ["auth_invalid", "auth_expired"],
|
|
185
185
|
},
|
|
186
186
|
"auth.rotate": {
|
|
187
187
|
plane: "common",
|
|
188
188
|
roles: INSTANCE_ONLY,
|
|
189
189
|
needs_hello: true,
|
|
190
|
-
locality: "
|
|
190
|
+
locality: "owner_instance",
|
|
191
191
|
errors: ["auth_invalid", "auth_expired"],
|
|
192
192
|
},
|
|
193
193
|
|
|
@@ -196,28 +196,28 @@ export const OP_ATTRIBUTES = {
|
|
|
196
196
|
plane: "messaging",
|
|
197
197
|
roles: AGENT_AND_USER,
|
|
198
198
|
needs_hello: true,
|
|
199
|
-
locality: "
|
|
199
|
+
locality: "any_instance",
|
|
200
200
|
errors: ["session_not_found"],
|
|
201
201
|
},
|
|
202
202
|
"say.post": {
|
|
203
203
|
plane: "messaging",
|
|
204
204
|
roles: SESSION_ONLY,
|
|
205
205
|
needs_hello: true,
|
|
206
|
-
locality: "
|
|
206
|
+
locality: "any_instance",
|
|
207
207
|
errors: ["rate_limited"],
|
|
208
208
|
},
|
|
209
209
|
"say.unread.clear": {
|
|
210
210
|
plane: "messaging",
|
|
211
211
|
roles: USER_ONLY,
|
|
212
212
|
needs_hello: true,
|
|
213
|
-
locality: "
|
|
213
|
+
locality: "any_instance",
|
|
214
214
|
errors: [],
|
|
215
215
|
},
|
|
216
216
|
"notify.send": {
|
|
217
217
|
plane: "messaging",
|
|
218
218
|
roles: AGENT_AND_USER,
|
|
219
219
|
needs_hello: true,
|
|
220
|
-
locality: "
|
|
220
|
+
locality: "any_instance",
|
|
221
221
|
errors: ["rate_limited"],
|
|
222
222
|
},
|
|
223
223
|
|
|
@@ -226,7 +226,7 @@ export const OP_ATTRIBUTES = {
|
|
|
226
226
|
plane: "control",
|
|
227
227
|
roles: USER_ONLY,
|
|
228
228
|
needs_hello: true,
|
|
229
|
-
locality: "
|
|
229
|
+
locality: "owner_instance",
|
|
230
230
|
errors: ["session_not_found"],
|
|
231
231
|
},
|
|
232
232
|
"session.rename": {
|
|
@@ -234,42 +234,42 @@ export const OP_ATTRIBUTES = {
|
|
|
234
234
|
roles: USER_ONLY,
|
|
235
235
|
needs_hello: true,
|
|
236
236
|
capability: "terminal",
|
|
237
|
-
locality: "
|
|
237
|
+
locality: "owner_instance",
|
|
238
238
|
errors: ["session_not_found"],
|
|
239
239
|
},
|
|
240
240
|
"session.env.read": {
|
|
241
241
|
plane: "control",
|
|
242
242
|
roles: USER_ONLY,
|
|
243
243
|
needs_hello: true,
|
|
244
|
-
locality: "
|
|
244
|
+
locality: "owner_instance",
|
|
245
245
|
errors: ["session_not_found"],
|
|
246
246
|
},
|
|
247
247
|
"session.search": {
|
|
248
248
|
plane: "control",
|
|
249
249
|
roles: USER_ONLY,
|
|
250
250
|
needs_hello: true,
|
|
251
|
-
locality: "
|
|
251
|
+
locality: "owner_instance",
|
|
252
252
|
errors: [],
|
|
253
253
|
},
|
|
254
254
|
"session.dump.write": {
|
|
255
255
|
plane: "control",
|
|
256
256
|
roles: USER_ONLY,
|
|
257
257
|
needs_hello: true,
|
|
258
|
-
locality: "
|
|
258
|
+
locality: "owner_instance",
|
|
259
259
|
errors: ["not_found"],
|
|
260
260
|
},
|
|
261
261
|
"dump.presets.read": {
|
|
262
262
|
plane: "control",
|
|
263
263
|
roles: AGENT_AND_USER,
|
|
264
264
|
needs_hello: true,
|
|
265
|
-
locality: "
|
|
265
|
+
locality: "owner_instance",
|
|
266
266
|
errors: [],
|
|
267
267
|
},
|
|
268
268
|
"transcript.read": {
|
|
269
269
|
plane: "control",
|
|
270
270
|
roles: AGENT_AND_USER,
|
|
271
271
|
needs_hello: true,
|
|
272
|
-
locality: "
|
|
272
|
+
locality: "owner_instance",
|
|
273
273
|
scope: "role",
|
|
274
274
|
errors: ["not_found"],
|
|
275
275
|
},
|
|
@@ -277,7 +277,7 @@ export const OP_ATTRIBUTES = {
|
|
|
277
277
|
plane: "control",
|
|
278
278
|
roles: AGENT_AND_USER,
|
|
279
279
|
needs_hello: true,
|
|
280
|
-
locality: "
|
|
280
|
+
locality: "owner_instance",
|
|
281
281
|
scope: "role",
|
|
282
282
|
errors: ["not_found"],
|
|
283
283
|
},
|
|
@@ -286,14 +286,14 @@ export const OP_ATTRIBUTES = {
|
|
|
286
286
|
roles: USER_ONLY,
|
|
287
287
|
needs_hello: true,
|
|
288
288
|
capability: "fork",
|
|
289
|
-
locality: "
|
|
289
|
+
locality: "owner_instance",
|
|
290
290
|
errors: ["not_found"],
|
|
291
291
|
},
|
|
292
292
|
"session.forget": {
|
|
293
293
|
plane: "control",
|
|
294
294
|
roles: USER_ONLY,
|
|
295
295
|
needs_hello: true,
|
|
296
|
-
locality: "
|
|
296
|
+
locality: "owner_instance",
|
|
297
297
|
errors: [],
|
|
298
298
|
},
|
|
299
299
|
|
|
@@ -302,7 +302,7 @@ export const OP_ATTRIBUTES = {
|
|
|
302
302
|
plane: "control",
|
|
303
303
|
roles: AGENT_AND_USER,
|
|
304
304
|
needs_hello: true,
|
|
305
|
-
locality: "
|
|
305
|
+
locality: "owner_instance",
|
|
306
306
|
scope: "role",
|
|
307
307
|
errors: ["path_forbidden", "not_found"],
|
|
308
308
|
},
|
|
@@ -310,7 +310,7 @@ export const OP_ATTRIBUTES = {
|
|
|
310
310
|
plane: "control",
|
|
311
311
|
roles: AGENT_AND_USER,
|
|
312
312
|
needs_hello: true,
|
|
313
|
-
locality: "
|
|
313
|
+
locality: "owner_instance",
|
|
314
314
|
scope: "role",
|
|
315
315
|
errors: ["path_forbidden", "not_found"],
|
|
316
316
|
},
|
|
@@ -318,42 +318,42 @@ export const OP_ATTRIBUTES = {
|
|
|
318
318
|
plane: "control",
|
|
319
319
|
roles: USER_ONLY,
|
|
320
320
|
needs_hello: true,
|
|
321
|
-
locality: "
|
|
321
|
+
locality: "owner_instance",
|
|
322
322
|
errors: ["path_not_writable", "file_exists"],
|
|
323
323
|
},
|
|
324
324
|
"file.create": {
|
|
325
325
|
plane: "control",
|
|
326
326
|
roles: USER_ONLY,
|
|
327
327
|
needs_hello: true,
|
|
328
|
-
locality: "
|
|
328
|
+
locality: "owner_instance",
|
|
329
329
|
errors: ["file_exists", "path_forbidden"],
|
|
330
330
|
},
|
|
331
331
|
"file.edit": {
|
|
332
332
|
plane: "control",
|
|
333
333
|
roles: USER_ONLY,
|
|
334
334
|
needs_hello: true,
|
|
335
|
-
locality: "
|
|
335
|
+
locality: "owner_instance",
|
|
336
336
|
errors: ["file_conflict", "not_a_text_file"],
|
|
337
337
|
},
|
|
338
338
|
"file.delete": {
|
|
339
339
|
plane: "control",
|
|
340
340
|
roles: USER_ONLY,
|
|
341
341
|
needs_hello: true,
|
|
342
|
-
locality: "
|
|
342
|
+
locality: "owner_instance",
|
|
343
343
|
errors: ["path_forbidden", "not_found"],
|
|
344
344
|
},
|
|
345
345
|
"file.find": {
|
|
346
346
|
plane: "control",
|
|
347
347
|
roles: USER_ONLY,
|
|
348
348
|
needs_hello: true,
|
|
349
|
-
locality: "
|
|
349
|
+
locality: "owner_instance",
|
|
350
350
|
errors: ["path_forbidden"],
|
|
351
351
|
},
|
|
352
352
|
"file.stat": {
|
|
353
353
|
plane: "control",
|
|
354
354
|
roles: USER_ONLY,
|
|
355
355
|
needs_hello: true,
|
|
356
|
-
locality: "
|
|
356
|
+
locality: "owner_instance",
|
|
357
357
|
errors: [],
|
|
358
358
|
},
|
|
359
359
|
"dir.tree": {
|
|
@@ -361,7 +361,7 @@ export const OP_ATTRIBUTES = {
|
|
|
361
361
|
roles: USER_ONLY,
|
|
362
362
|
needs_hello: true,
|
|
363
363
|
capability: "launcher",
|
|
364
|
-
locality: "
|
|
364
|
+
locality: "owner_instance",
|
|
365
365
|
errors: [],
|
|
366
366
|
},
|
|
367
367
|
|
|
@@ -371,7 +371,7 @@ export const OP_ATTRIBUTES = {
|
|
|
371
371
|
roles: USER_ONLY,
|
|
372
372
|
needs_hello: true,
|
|
373
373
|
capability: "launcher",
|
|
374
|
-
locality: "
|
|
374
|
+
locality: "owner_instance",
|
|
375
375
|
errors: [],
|
|
376
376
|
},
|
|
377
377
|
"launcher.run": {
|
|
@@ -379,7 +379,7 @@ export const OP_ATTRIBUTES = {
|
|
|
379
379
|
roles: USER_ONLY,
|
|
380
380
|
needs_hello: true,
|
|
381
381
|
capability: "launcher",
|
|
382
|
-
locality: "
|
|
382
|
+
locality: "owner_instance",
|
|
383
383
|
errors: [],
|
|
384
384
|
},
|
|
385
385
|
"sandbox.grant": {
|
|
@@ -387,7 +387,7 @@ export const OP_ATTRIBUTES = {
|
|
|
387
387
|
roles: USER_ONLY,
|
|
388
388
|
needs_hello: true,
|
|
389
389
|
capability: "sandbox",
|
|
390
|
-
locality: "
|
|
390
|
+
locality: "owner_instance",
|
|
391
391
|
errors: ["path_forbidden"],
|
|
392
392
|
},
|
|
393
393
|
"sandbox.revoke": {
|
|
@@ -395,7 +395,7 @@ export const OP_ATTRIBUTES = {
|
|
|
395
395
|
roles: USER_ONLY,
|
|
396
396
|
needs_hello: true,
|
|
397
397
|
capability: "sandbox",
|
|
398
|
-
locality: "
|
|
398
|
+
locality: "owner_instance",
|
|
399
399
|
errors: [],
|
|
400
400
|
},
|
|
401
401
|
"translate.run": {
|
|
@@ -403,7 +403,7 @@ export const OP_ATTRIBUTES = {
|
|
|
403
403
|
roles: USER_ONLY,
|
|
404
404
|
needs_hello: true,
|
|
405
405
|
capability: "translate",
|
|
406
|
-
locality: "
|
|
406
|
+
locality: "owner_instance",
|
|
407
407
|
errors: ["translate_helper_failed"],
|
|
408
408
|
},
|
|
409
409
|
"llm.usage.read": {
|
|
@@ -411,7 +411,7 @@ export const OP_ATTRIBUTES = {
|
|
|
411
411
|
roles: USER_ONLY,
|
|
412
412
|
needs_hello: true,
|
|
413
413
|
capability: "llm_usage",
|
|
414
|
-
locality: "
|
|
414
|
+
locality: "owner_instance",
|
|
415
415
|
errors: [],
|
|
416
416
|
},
|
|
417
417
|
"llm.stats.read": {
|
|
@@ -419,32 +419,32 @@ export const OP_ATTRIBUTES = {
|
|
|
419
419
|
roles: USER_ONLY,
|
|
420
420
|
needs_hello: true,
|
|
421
421
|
capability: "llm_stats",
|
|
422
|
-
locality: "
|
|
422
|
+
locality: "owner_instance",
|
|
423
423
|
errors: [],
|
|
424
424
|
},
|
|
425
425
|
|
|
426
426
|
// --- control: the shared key-value store (3) ---
|
|
427
|
-
// The only control ops
|
|
427
|
+
// The only control ops not answered by an owning instance: a value is held by every
|
|
428
428
|
// instance rather than by one, so whichever is asked can answer.
|
|
429
429
|
"kv.read": {
|
|
430
430
|
plane: "control",
|
|
431
431
|
roles: USER_ONLY,
|
|
432
432
|
needs_hello: true,
|
|
433
|
-
locality: "
|
|
433
|
+
locality: "any_instance",
|
|
434
434
|
errors: ["not_found"],
|
|
435
435
|
},
|
|
436
436
|
"kv.write": {
|
|
437
437
|
plane: "control",
|
|
438
438
|
roles: USER_ONLY,
|
|
439
439
|
needs_hello: true,
|
|
440
|
-
locality: "
|
|
440
|
+
locality: "any_instance",
|
|
441
441
|
errors: [],
|
|
442
442
|
},
|
|
443
443
|
"kv.delete": {
|
|
444
444
|
plane: "control",
|
|
445
445
|
roles: USER_ONLY,
|
|
446
446
|
needs_hello: true,
|
|
447
|
-
locality: "
|
|
447
|
+
locality: "any_instance",
|
|
448
448
|
errors: [],
|
|
449
449
|
},
|
|
450
450
|
} as const satisfies Record<string, OpAttributes>;
|
|
@@ -466,7 +466,7 @@ export function opErrors(op: OpName): ErrorCode[] {
|
|
|
466
466
|
if (attrs.needs_hello) codes.add("hello_required");
|
|
467
467
|
if (attrs.roles.length < ALL_ROLES.length) codes.add("forbidden");
|
|
468
468
|
if (attrs.capability !== undefined) codes.add("capability_unavailable");
|
|
469
|
-
if (attrs.locality === "
|
|
469
|
+
if (attrs.locality === "owner_instance") codes.add("instance_unreachable");
|
|
470
470
|
return [...codes];
|
|
471
471
|
}
|
|
472
472
|
|
package/src/common/auth.ts
CHANGED
|
@@ -30,7 +30,7 @@ export const REGISTER_TTL_MS = 10 * 60 * 1000;
|
|
|
30
30
|
* retention would otherwise carry the removed credential back as news. */
|
|
31
31
|
export const FAMILY_TOMBSTONE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
32
32
|
|
|
33
|
-
/** Who a person is to this
|
|
33
|
+
/** Who a person is to this mesh. Issued when the registration URL is made
|
|
34
34
|
* (`<unit>-<counter>` by default) and carried by every record they own. */
|
|
35
35
|
export const Subject = Type.String({ $id: "Subject", minLength: 1, maxLength: 128 });
|
|
36
36
|
export type Subject = Static<typeof Subject>;
|
|
@@ -337,11 +337,11 @@ export const AuthRotateResponse = response("auth.rotate", AuthRotateResult);
|
|
|
337
337
|
|
|
338
338
|
// --- the replicated records ------------------------------------------------
|
|
339
339
|
|
|
340
|
-
/** A registered passkey, as every instance in the
|
|
340
|
+
/** A registered passkey, as every instance in the mesh holds it.
|
|
341
341
|
*
|
|
342
342
|
* Complete once it is written: the instance that registered it is not asked
|
|
343
343
|
* about it again, which is what lets a person authenticate anywhere in the
|
|
344
|
-
*
|
|
344
|
+
* mesh while the instance they registered at is down. */
|
|
345
345
|
export const CredentialRecord = Type.Object(
|
|
346
346
|
{
|
|
347
347
|
kind: Type.Literal("credential"),
|
|
@@ -503,7 +503,7 @@ export type AuthTombstone = Static<typeof AuthTombstone>;
|
|
|
503
503
|
/** One entry of the replicated set, under the key it is matched by. */
|
|
504
504
|
export const AuthRecord = Type.Object(
|
|
505
505
|
{
|
|
506
|
-
/** What this entry is,
|
|
506
|
+
/** What this entry is, mesh-wide. Two instances writing one key hold the
|
|
507
507
|
* same thing, and the later `updated_at` is what stands. */
|
|
508
508
|
key: Type.String({ minLength: 1, maxLength: 256 }),
|
|
509
509
|
updated_at: Timestamp,
|
package/src/common/ping.ts
CHANGED
|
@@ -7,8 +7,8 @@ export type InstancePingArgs = Static<typeof InstancePingArgs>;
|
|
|
7
7
|
|
|
8
8
|
/** How the answering daemon process is running.
|
|
9
9
|
*
|
|
10
|
-
* This is about one process, which is why the op is
|
|
11
|
-
* of the
|
|
10
|
+
* This is about one process, which is why the op is `owner_instance`: the health
|
|
11
|
+
* of the mesh is `hello`'s `instances[]`, not a ping fanned out. */
|
|
12
12
|
export const InstancePingResult = Type.Object({
|
|
13
13
|
instance: InstanceId,
|
|
14
14
|
version: Type.String(),
|
package/src/common/topics.ts
CHANGED
|
@@ -93,6 +93,12 @@ export interface TopicAttributes {
|
|
|
93
93
|
* `forbidden`, and one naming a capability the instance lacks answers
|
|
94
94
|
* `capability_unavailable`. */
|
|
95
95
|
export const TOPIC_ATTRIBUTES = {
|
|
96
|
+
// The one topic where the same subscription means two things: a session is
|
|
97
|
+
// handed what was addressed to it, so what it reads leaves its inbox, while a
|
|
98
|
+
// person is looking at somebody else's mail and reading it moves nothing.
|
|
99
|
+
// Stated on the frame rather than as an attribute here — it is not a fold and
|
|
100
|
+
// not an authorization, and a subscriber does the same thing with the frames
|
|
101
|
+
// whichever it is.
|
|
96
102
|
inbox: { roles: ["session", "user"], granularity: "element" },
|
|
97
103
|
notify: { roles: ["session", "user"], granularity: "event" },
|
|
98
104
|
// A row here changes on its own — one session becomes busy while the rest
|
package/src/control/dump.ts
CHANGED
|
@@ -576,7 +576,28 @@ export type DumpIdEntry = Static<typeof DumpIdEntry>;
|
|
|
576
576
|
export const DumpIds = Type.Array(DumpIdEntry, { $id: "DumpIds" });
|
|
577
577
|
export type DumpIds = Static<typeof DumpIds>;
|
|
578
578
|
|
|
579
|
-
/**
|
|
579
|
+
/** How the selected items are written out.
|
|
580
|
+
*
|
|
581
|
+
* The selection is one thing and its rendering another: which items a dump is
|
|
582
|
+
* of follows from the range and the `types`, and a format decides only what the
|
|
583
|
+
* file then says about them. So all three are dumps of the same items, and the
|
|
584
|
+
* reply describes that selection whichever was asked for.
|
|
585
|
+
*
|
|
586
|
+
* `items` is this contract's own vocabulary, the typed items as
|
|
587
|
+
* `SessionDumpFile`. `records` writes the transcript records those items were
|
|
588
|
+
* read from, unchanged, one JSON document per line — for a tool that already
|
|
589
|
+
* reads the harness's file and wants the classifying alone, which is why
|
|
590
|
+
* nothing of ours is added around them. An item names its record, so several
|
|
591
|
+
* items out of one record are one record here and the line count is not the
|
|
592
|
+
* item count. `text` renders the items for a person to read. */
|
|
593
|
+
export const SessionDumpFormat = Type.Union(
|
|
594
|
+
[Type.Literal("items"), Type.Literal("records"), Type.Literal("text")],
|
|
595
|
+
{ $id: "SessionDumpFormat" },
|
|
596
|
+
);
|
|
597
|
+
export type SessionDumpFormat = Static<typeof SessionDumpFormat>;
|
|
598
|
+
|
|
599
|
+
/** The file an `items` dump is written to. The other two formats are not this
|
|
600
|
+
* shape: `records` is the harness's own lines and `text` is prose.
|
|
580
601
|
*
|
|
581
602
|
* The reply to a dump names a path rather than carrying the items, so the file
|
|
582
603
|
* is where they actually travel — which makes its shape as much a part of the
|
package/src/control/session.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
import { request, response } from "../envelope.ts";
|
|
3
3
|
import { InstanceId, Sid, Timestamp } from "../identifiers.ts";
|
|
4
|
-
import { DumpIds, TranscriptItemSelector, TranscriptItemType } from "./dump.ts";
|
|
4
|
+
import { DumpIds, SessionDumpFormat, TranscriptItemSelector, TranscriptItemType } from "./dump.ts";
|
|
5
5
|
|
|
6
6
|
/** Ends the OS process behind a session.
|
|
7
7
|
*
|
|
@@ -203,6 +203,8 @@ export const SessionDumpWriteArgs = Type.Object({
|
|
|
203
203
|
/** Leave out the machinery of in-process agents, which
|
|
204
204
|
* `["-message.sub", "-tool.Agent"]` also says. */
|
|
205
205
|
no_agent: Type.Optional(Type.Boolean()),
|
|
206
|
+
/** What the file says about the items selected. Absent is `items`. */
|
|
207
|
+
format: Type.Optional(SessionDumpFormat),
|
|
206
208
|
});
|
|
207
209
|
export type SessionDumpWriteArgs = Static<typeof SessionDumpWriteArgs>;
|
|
208
210
|
|
|
@@ -210,12 +212,18 @@ export const SessionDumpWriteResult = Type.Object({
|
|
|
210
212
|
/** Absolute path on the writing instance's host. */
|
|
211
213
|
path: Type.String(),
|
|
212
214
|
instance: InstanceId,
|
|
213
|
-
/** How many items of each type were
|
|
215
|
+
/** How many items of each type were selected, keyed by type name. A single
|
|
214
216
|
* total leaves the caller unable to tell a dump that kept what it asked for
|
|
215
|
-
* from one whose selection matched almost nothing.
|
|
217
|
+
* from one whose selection matched almost nothing.
|
|
218
|
+
*
|
|
219
|
+
* It counts items and not what the file holds, whatever the `format`: the
|
|
220
|
+
* selection is what a caller asked for and what it reads this against, and a
|
|
221
|
+
* count that moved with the rendering would answer a different question each
|
|
222
|
+
* time. */
|
|
216
223
|
entries: Type.Record(TranscriptItemType, Type.Integer({ minimum: 0 })),
|
|
217
224
|
/** The ids those items carried, so the next dump — of an agent named here —
|
|
218
|
-
* can be asked for without opening the file.
|
|
225
|
+
* can be asked for without opening the file. Read off the selection like
|
|
226
|
+
* `entries`, whatever the `format`. */
|
|
219
227
|
ids: DumpIds,
|
|
220
228
|
bytes: Type.Integer({ minimum: 0 }),
|
|
221
229
|
});
|
package/src/envelope.ts
CHANGED
|
@@ -62,7 +62,7 @@ export const RequestEnvelope = Type.Object(
|
|
|
62
62
|
*
|
|
63
63
|
* A forwarded request that names none is dispatched as the `instance` role
|
|
64
64
|
* it arrived on, which the attribute table already answers: an
|
|
65
|
-
*
|
|
65
|
+
* an `owner_instance` op called by an instance is `forbidden`. */
|
|
66
66
|
caller: Type.Optional(CallerIdentity),
|
|
67
67
|
},
|
|
68
68
|
{ $id: "RequestEnvelope" },
|
package/src/errors.ts
CHANGED
|
@@ -35,14 +35,14 @@ export const ERROR_CODES = [
|
|
|
35
35
|
"invalid_args",
|
|
36
36
|
/** The op declares a `capability` this instance does not have. */
|
|
37
37
|
"capability_unavailable",
|
|
38
|
-
/** An `
|
|
38
|
+
/** An `owner_instance` op could not be forwarded to the instance that owns
|
|
39
39
|
* the subject. */
|
|
40
40
|
"instance_unreachable",
|
|
41
41
|
// --- subscription ---
|
|
42
42
|
/** The topic name is not one this protocol generation defines. */
|
|
43
43
|
"topic_unknown",
|
|
44
44
|
// --- subject lookup ---
|
|
45
|
-
/** The `sid` names no session anywhere in the
|
|
45
|
+
/** The `sid` names no session anywhere in the mesh. */
|
|
46
46
|
"session_not_found",
|
|
47
47
|
/** The path, transcript, or record named by the arguments does not exist. */
|
|
48
48
|
"not_found",
|
|
@@ -66,7 +66,7 @@ export const ERROR_CODES = [
|
|
|
66
66
|
* and `msg` says no more than the instance's own log would want. */
|
|
67
67
|
"auth_invalid",
|
|
68
68
|
/** The instance that issued the challenge or registration, and alone can
|
|
69
|
-
* spend it, is not one this
|
|
69
|
+
* spend it, is not one this mesh knows or could reach just now. The client
|
|
70
70
|
* asks for a fresh one, which the instance it is talking to can issue. */
|
|
71
71
|
"auth_unknown_issuer",
|
|
72
72
|
// --- translate ---
|
package/src/fixtures/topics.ts
CHANGED
|
@@ -29,6 +29,7 @@ export const INBOX_FRAME: Static<typeof InboxFrame> = {
|
|
|
29
29
|
from_label: "contract-fixtures",
|
|
30
30
|
text: "fixture を export した",
|
|
31
31
|
sent_at: FIXTURE_NOW,
|
|
32
|
+
to: sid,
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
35
|
mid: `${instance}/1842`,
|
|
@@ -37,15 +38,34 @@ export const INBOX_FRAME: Static<typeof InboxFrame> = {
|
|
|
37
38
|
text: "確認する",
|
|
38
39
|
reply_to: mid,
|
|
39
40
|
sent_at: FIXTURE_NOW + 1_000,
|
|
41
|
+
to: sid,
|
|
40
42
|
},
|
|
41
43
|
],
|
|
42
44
|
};
|
|
43
45
|
|
|
46
|
+
/** A later frame, where one message has been handed over and another was never
|
|
47
|
+
* taken. */
|
|
48
|
+
export const INBOX_REMOVED_FRAME: Static<typeof InboxFrame> = {
|
|
49
|
+
ev: "topic",
|
|
50
|
+
topic: "inbox",
|
|
51
|
+
instance,
|
|
52
|
+
data: [
|
|
53
|
+
{ mid, removed: true, reason: "delivered" },
|
|
54
|
+
{ mid: `${instance}/1842`, removed: true, reason: "expired" },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
44
58
|
export const NOTIFY_FRAME: Static<typeof NotifyFrame> = {
|
|
45
59
|
ev: "topic",
|
|
46
60
|
topic: "notify",
|
|
47
61
|
instance,
|
|
48
|
-
data: {
|
|
62
|
+
data: {
|
|
63
|
+
sid,
|
|
64
|
+
sid_label: "contract-fixtures",
|
|
65
|
+
text: "確認して",
|
|
66
|
+
reply_to: mid,
|
|
67
|
+
sent_at: FIXTURE_NOW,
|
|
68
|
+
},
|
|
49
69
|
};
|
|
50
70
|
|
|
51
71
|
const PEER = {
|
package/src/identifiers.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
3
|
/** A session id: the uuid Claude Code gives its own session. Globally unique,
|
|
4
|
-
* so it names a session across the whole
|
|
4
|
+
* so it names a session across the whole mesh without an instance prefix. */
|
|
5
5
|
export const Sid = Type.String({
|
|
6
6
|
$id: "Sid",
|
|
7
7
|
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
package/src/messaging/message.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { InstanceId, Mid, Sender, Sid, Timestamp } from "../identifiers.ts";
|
|
|
7
7
|
* These are not errors: the op succeeded and the message is held in the
|
|
8
8
|
* recipient's inbox. They tell the sender what to do next — wait, resend to
|
|
9
9
|
* another session, or give up. The op itself fails only when `to` names no
|
|
10
|
-
* session anywhere in the
|
|
10
|
+
* session anywhere in the mesh (`session_not_found`). */
|
|
11
11
|
export const UndeliveredReason = Type.Union(
|
|
12
12
|
[
|
|
13
13
|
/** Alive, but not yet listening. The daemon delivers when it starts. */
|
|
@@ -89,11 +89,57 @@ export const InboxMessage = Type.Object(
|
|
|
89
89
|
text: Type.String(),
|
|
90
90
|
reply_to: Type.Optional(Mid),
|
|
91
91
|
sent_at: Timestamp,
|
|
92
|
+
/** Who it is addressed to. A session's own subscription is already the
|
|
93
|
+
* recipient, so a row reaching one says nothing by repeating it; a person
|
|
94
|
+
* holds the inbox of every session in one subscription, and a row that
|
|
95
|
+
* does not name its recipient cannot be placed against any of them. So the
|
|
96
|
+
* instance states it on the rows it answers a person with. */
|
|
97
|
+
to: Type.Optional(Sid),
|
|
92
98
|
},
|
|
93
99
|
{ $id: "InboxMessage" },
|
|
94
100
|
);
|
|
95
101
|
export type InboxMessage = Static<typeof InboxMessage>;
|
|
96
102
|
|
|
103
|
+
/** Why a message is no longer in the inbox.
|
|
104
|
+
*
|
|
105
|
+
* The three are apart because they are three different things to have happened
|
|
106
|
+
* to a message, and a reader watching for one it sent draws each differently:
|
|
107
|
+
* `delivered` means the recipient has it and its own account of it follows,
|
|
108
|
+
* while the other two mean it never arrived and never will. A removal with no
|
|
109
|
+
* reason would leave a waiting message and an abandoned one looking alike. */
|
|
110
|
+
export const InboxRemovedReason = Type.Union(
|
|
111
|
+
[
|
|
112
|
+
/** Handed to the recipient. */
|
|
113
|
+
Type.Literal("delivered"),
|
|
114
|
+
/** `INBOX_RETENTION_MS` ran out with the recipient never taking it. */
|
|
115
|
+
Type.Literal("expired"),
|
|
116
|
+
/** Dropped, oldest first, to take a newer message into a full inbox —
|
|
117
|
+
* the same event the newer message's sender was told as `inbox_full`. */
|
|
118
|
+
Type.Literal("dropped"),
|
|
119
|
+
],
|
|
120
|
+
{ $id: "InboxRemovedReason" },
|
|
121
|
+
);
|
|
122
|
+
export type InboxRemovedReason = Static<typeof InboxRemovedReason>;
|
|
123
|
+
|
|
124
|
+
/** A message that has left the inbox.
|
|
125
|
+
*
|
|
126
|
+
* A removal has to be a marked element rather than an absence, since a frame
|
|
127
|
+
* carries only what changed and an absence in it says nothing. It names the
|
|
128
|
+
* `mid` every row is matched by, and why — there is no message left to
|
|
129
|
+
* describe, and the reason is the one thing the reader cannot derive. */
|
|
130
|
+
export const InboxRemoved = Type.Object(
|
|
131
|
+
{
|
|
132
|
+
mid: Mid,
|
|
133
|
+
removed: Type.Literal(true),
|
|
134
|
+
reason: InboxRemovedReason,
|
|
135
|
+
},
|
|
136
|
+
{ $id: "InboxRemoved" },
|
|
137
|
+
);
|
|
138
|
+
export type InboxRemoved = Static<typeof InboxRemoved>;
|
|
139
|
+
|
|
140
|
+
export const InboxElement = Type.Union([InboxMessage, InboxRemoved], { $id: "InboxElement" });
|
|
141
|
+
export type InboxElement = Static<typeof InboxElement>;
|
|
142
|
+
|
|
97
143
|
/** How long an undelivered message is kept for its recipient. The same window
|
|
98
144
|
* a lost session stays listed for: a message outliving the session it was
|
|
99
145
|
* addressed to would be offered to no one, and a session outliving what was
|
|
@@ -106,6 +152,21 @@ export const INBOX_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
106
152
|
* accepts is one the recipient can still be handed. */
|
|
107
153
|
export const INBOX_MAX_PER_SID = 256;
|
|
108
154
|
|
|
109
|
-
/** The `inbox` topic
|
|
110
|
-
*
|
|
111
|
-
|
|
155
|
+
/** The `inbox` topic: what is waiting, for whoever may see it.
|
|
156
|
+
*
|
|
157
|
+
* Elements, matched by `mid`. The snapshot is what is still undelivered and
|
|
158
|
+
* each later frame is what changed — a message arriving, or one leaving as an
|
|
159
|
+
* `InboxRemoved`.
|
|
160
|
+
*
|
|
161
|
+
* **A session's subscription is the delivery and a person's is a view.** What
|
|
162
|
+
* the session is handed it has been given, and the message leaves its inbox;
|
|
163
|
+
* what a person reads leaves the inbox exactly as it was, because a person is
|
|
164
|
+
* not who any of it was addressed to. The asymmetry is the point rather than an
|
|
165
|
+
* exception: without the view there is no way to see that something sent is
|
|
166
|
+
* still waiting, and a view that consumed what it looked at would deliver
|
|
167
|
+
* messages to no one by being opened.
|
|
168
|
+
*
|
|
169
|
+
* A person therefore sees a message twice over: waiting here, and afterwards in
|
|
170
|
+
* the recipient's own transcript. `mid` is what joins the two, and the removal
|
|
171
|
+
* marked `delivered` is what says the second is coming. */
|
|
172
|
+
export const InboxFrame = topicFrame("inbox", Type.Array(InboxElement));
|
package/src/messaging/notify.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
import { request, response, topicFrame } from "../envelope.ts";
|
|
3
|
-
import { Sid, Timestamp } from "../identifiers.ts";
|
|
3
|
+
import { Mid, Sid, Timestamp } from "../identifiers.ts";
|
|
4
4
|
|
|
5
5
|
/** A short line meant to reach a person watching, not the session's own turn.
|
|
6
6
|
* Delivery is best effort and unacknowledged; unlike `message.send`, nothing is
|
|
@@ -9,6 +9,8 @@ export const NotifySendArgs = Type.Object({
|
|
|
9
9
|
/** The session the notification is about. Omit to mean the caller. */
|
|
10
10
|
sid: Type.Optional(Sid),
|
|
11
11
|
text: Type.String({ minLength: 1 }),
|
|
12
|
+
/** The `mid` this line answers, when it answers one. */
|
|
13
|
+
reply_to: Type.Optional(Mid),
|
|
12
14
|
});
|
|
13
15
|
export type NotifySendArgs = Static<typeof NotifySendArgs>;
|
|
14
16
|
|
|
@@ -24,6 +26,15 @@ export const Notification = Type.Object(
|
|
|
24
26
|
/** How the session should be shown, resolved by the issuing instance. */
|
|
25
27
|
sid_label: Type.String(),
|
|
26
28
|
text: Type.String(),
|
|
29
|
+
/** What this line answers, when it answers something. A notification is
|
|
30
|
+
* shown while the session's own account of the same answer is still being
|
|
31
|
+
* written, so a reader holding both needs to know they are one thing: the
|
|
32
|
+
* `mid` is the key it matches on, and without it the two stand as two.
|
|
33
|
+
*
|
|
34
|
+
* It says what is answered and never what kind of line this is. A
|
|
35
|
+
* notification is one thing whoever it came from, and a kind would be read
|
|
36
|
+
* as a reason to draw it differently. */
|
|
37
|
+
reply_to: Type.Optional(Mid),
|
|
27
38
|
sent_at: Timestamp,
|
|
28
39
|
},
|
|
29
40
|
{ $id: "Notification" },
|