@ccmsg/protocol 1.22.1 → 2.0.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 +68 -68
- package/src/common/auth.ts +4 -4
- package/src/common/hello.ts +38 -5
- package/src/common/ping.ts +2 -2
- package/src/common/topics.ts +6 -0
- package/src/control/agents.ts +20 -11
- package/src/control/dump.ts +22 -1
- package/src/control/peers.ts +124 -34
- package/src/control/session.ts +24 -9
- package/src/envelope.ts +2 -2
- package/src/errors.ts +13 -3
- package/src/fixtures/common.ts +5 -4
- package/src/fixtures/control.ts +1 -1
- package/src/fixtures/messaging.ts +1 -0
- package/src/fixtures/topics.ts +67 -9
- package/src/identifiers.ts +16 -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,29 +196,29 @@ export const OP_ATTRIBUTES = {
|
|
|
196
196
|
plane: "messaging",
|
|
197
197
|
roles: AGENT_AND_USER,
|
|
198
198
|
needs_hello: true,
|
|
199
|
-
locality: "
|
|
200
|
-
errors: ["session_not_found"],
|
|
199
|
+
locality: "any_instance",
|
|
200
|
+
errors: ["session_not_found", "session_duplicated"],
|
|
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: "
|
|
221
|
-
errors: ["rate_limited"],
|
|
220
|
+
locality: "any_instance",
|
|
221
|
+
errors: ["rate_limited", "session_duplicated"],
|
|
222
222
|
},
|
|
223
223
|
|
|
224
224
|
// --- control: session observation and operation (10) ---
|
|
@@ -226,50 +226,50 @@ export const OP_ATTRIBUTES = {
|
|
|
226
226
|
plane: "control",
|
|
227
227
|
roles: USER_ONLY,
|
|
228
228
|
needs_hello: true,
|
|
229
|
-
locality: "
|
|
230
|
-
errors: ["session_not_found"],
|
|
229
|
+
locality: "owner_instance",
|
|
230
|
+
errors: ["session_not_found", "ambiguous_run"],
|
|
231
231
|
},
|
|
232
232
|
"session.rename": {
|
|
233
233
|
plane: "control",
|
|
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: "
|
|
259
|
-
errors: ["not_found"],
|
|
258
|
+
locality: "owner_instance",
|
|
259
|
+
errors: ["not_found", "session_duplicated"],
|
|
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,66 +302,66 @@ 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
|
-
errors: ["path_forbidden", "not_found"],
|
|
307
|
+
errors: ["path_forbidden", "not_found", "session_duplicated"],
|
|
308
308
|
},
|
|
309
309
|
"file.read": {
|
|
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
|
-
errors: ["path_forbidden", "not_found"],
|
|
315
|
+
errors: ["path_forbidden", "not_found", "session_duplicated"],
|
|
316
316
|
},
|
|
317
317
|
"file.write": {
|
|
318
318
|
plane: "control",
|
|
319
319
|
roles: USER_ONLY,
|
|
320
320
|
needs_hello: true,
|
|
321
|
-
locality: "
|
|
322
|
-
errors: ["path_not_writable", "file_exists"],
|
|
321
|
+
locality: "owner_instance",
|
|
322
|
+
errors: ["path_not_writable", "file_exists", "session_duplicated"],
|
|
323
323
|
},
|
|
324
324
|
"file.create": {
|
|
325
325
|
plane: "control",
|
|
326
326
|
roles: USER_ONLY,
|
|
327
327
|
needs_hello: true,
|
|
328
|
-
locality: "
|
|
329
|
-
errors: ["file_exists", "path_forbidden"],
|
|
328
|
+
locality: "owner_instance",
|
|
329
|
+
errors: ["file_exists", "path_forbidden", "session_duplicated"],
|
|
330
330
|
},
|
|
331
331
|
"file.edit": {
|
|
332
332
|
plane: "control",
|
|
333
333
|
roles: USER_ONLY,
|
|
334
334
|
needs_hello: true,
|
|
335
|
-
locality: "
|
|
336
|
-
errors: ["file_conflict", "not_a_text_file"],
|
|
335
|
+
locality: "owner_instance",
|
|
336
|
+
errors: ["file_conflict", "not_a_text_file", "session_duplicated"],
|
|
337
337
|
},
|
|
338
338
|
"file.delete": {
|
|
339
339
|
plane: "control",
|
|
340
340
|
roles: USER_ONLY,
|
|
341
341
|
needs_hello: true,
|
|
342
|
-
locality: "
|
|
343
|
-
errors: ["path_forbidden", "not_found"],
|
|
342
|
+
locality: "owner_instance",
|
|
343
|
+
errors: ["path_forbidden", "not_found", "session_duplicated"],
|
|
344
344
|
},
|
|
345
345
|
"file.find": {
|
|
346
346
|
plane: "control",
|
|
347
347
|
roles: USER_ONLY,
|
|
348
348
|
needs_hello: true,
|
|
349
|
-
locality: "
|
|
350
|
-
errors: ["path_forbidden"],
|
|
349
|
+
locality: "owner_instance",
|
|
350
|
+
errors: ["path_forbidden", "session_duplicated"],
|
|
351
351
|
},
|
|
352
352
|
"file.stat": {
|
|
353
353
|
plane: "control",
|
|
354
354
|
roles: USER_ONLY,
|
|
355
355
|
needs_hello: true,
|
|
356
|
-
locality: "
|
|
357
|
-
errors: [],
|
|
356
|
+
locality: "owner_instance",
|
|
357
|
+
errors: ["session_duplicated"],
|
|
358
358
|
},
|
|
359
359
|
"dir.tree": {
|
|
360
360
|
plane: "control",
|
|
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/hello.ts
CHANGED
|
@@ -66,6 +66,16 @@ export const HelloSessionArgs = Type.Object({
|
|
|
66
66
|
...GREETING_FIELDS,
|
|
67
67
|
/** The session the connection speaks for. */
|
|
68
68
|
sid: Sid,
|
|
69
|
+
/** The harness process running this session. Where the greeting is carried by
|
|
70
|
+
* something standing in for the session — a hook, the CLI — it is the parent
|
|
71
|
+
* harness process's pid and not the helper's: what this names is the run, and
|
|
72
|
+
* the helper is not one.
|
|
73
|
+
*
|
|
74
|
+
* It is what ties this session to a process a launcher started before the
|
|
75
|
+
* harness had written anything of its own, and what lets two runs of one
|
|
76
|
+
* session be told apart. Optional: a session that cannot read it is greeted
|
|
77
|
+
* anyway, and the instance then knows the run by its connection alone. */
|
|
78
|
+
pid: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
69
79
|
/** What the session says about itself. All optional: a session states what it
|
|
70
80
|
* knows, and the instance derives or leaves unknown what it is not told. The
|
|
71
81
|
* instance repeats these on the `peers` topic, so they are the same fields
|
|
@@ -141,11 +151,11 @@ export const HelloResult = Type.Object({
|
|
|
141
151
|
version: Type.String(),
|
|
142
152
|
started_at: Timestamp,
|
|
143
153
|
/** Where a person opens the terminal a session runs in: the base URL of the
|
|
144
|
-
* gateway that fronts this instance's terminals. A
|
|
145
|
-
* itself in `terminal_id
|
|
146
|
-
*
|
|
147
|
-
* trailing slash, the path below it being the
|
|
148
|
-
* contract's.
|
|
154
|
+
* gateway that fronts this instance's terminals. A run's terminal names
|
|
155
|
+
* itself in `terminal_id`, and `terminalUrl` below composes the URL from the
|
|
156
|
+
* two — only for a handle under the `hyoui` scheme, this gateway serving no
|
|
157
|
+
* other. The base URL carries no trailing slash, the path below it being the
|
|
158
|
+
* gateway's spelling and not this contract's.
|
|
149
159
|
*
|
|
150
160
|
* Stated by the instance because only it knows which gateway stands in front
|
|
151
161
|
* of the machine its sessions run on; a client has no way to derive one from
|
|
@@ -164,6 +174,29 @@ export const HelloResult = Type.Object({
|
|
|
164
174
|
});
|
|
165
175
|
export type HelloResult = Static<typeof HelloResult>;
|
|
166
176
|
|
|
177
|
+
/** The terminal scheme the gateway in `terminal_gateway` serves. */
|
|
178
|
+
export const HYOUI_TERMINAL_SCHEME = "hyoui";
|
|
179
|
+
|
|
180
|
+
/** Where a person opens a run's terminal, or nothing.
|
|
181
|
+
*
|
|
182
|
+
* Nothing when the gateway is absent, and nothing when the handle is under
|
|
183
|
+
* another scheme: a handle names whose terminal it is, and composing this URL
|
|
184
|
+
* for one the gateway does not serve would hand a person a link to a page that
|
|
185
|
+
* is not there. A client holding such a handle opens it the way that system
|
|
186
|
+
* does, or leaves it unopened.
|
|
187
|
+
*
|
|
188
|
+
* Composed here rather than once per client, so the one place that knows both
|
|
189
|
+
* the scheme and the gateway's path is this contract. */
|
|
190
|
+
export function terminalUrl(
|
|
191
|
+
gateway: string | undefined,
|
|
192
|
+
terminalId: string | undefined,
|
|
193
|
+
): string | undefined {
|
|
194
|
+
if (gateway === undefined || terminalId === undefined) return undefined;
|
|
195
|
+
const prefix = `${HYOUI_TERMINAL_SCHEME}:`;
|
|
196
|
+
if (!terminalId.startsWith(prefix)) return undefined;
|
|
197
|
+
return `${gateway}/sessions/${terminalId.slice(prefix.length)}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
167
200
|
export const HelloSessionRequest = request("hello.session", HelloSessionArgs);
|
|
168
201
|
export const HelloSessionResponse = response("hello.session", HelloResult);
|
|
169
202
|
|
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/agents.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { type Static, Type } from "@sinclair/typebox";
|
|
2
2
|
import { topicFrame } from "../envelope.ts";
|
|
3
|
-
import { InstanceId, Sid, Timestamp } from "../identifiers.ts";
|
|
3
|
+
import { InstanceId, Sid, TerminalId, Timestamp } from "../identifiers.ts";
|
|
4
4
|
import { upstream } from "../upstream.ts";
|
|
5
5
|
|
|
6
|
-
/** One session as the harness itself reports it, noted
|
|
7
|
-
* it was found under.
|
|
6
|
+
/** One run of a session — one process — as the harness itself reports it, noted
|
|
7
|
+
* with which config home it was found under.
|
|
8
8
|
*
|
|
9
9
|
* This is the harness's view rather than the instance's: it covers sessions
|
|
10
10
|
* that never connected here, and it carries what only the process knows — its
|
|
11
11
|
* pid, its terminal, the title the session gave itself. The instance renames
|
|
12
12
|
* the fields as it copies them in; the words inside them stay the harness's,
|
|
13
|
-
* which is why the status-like fields are open sets.
|
|
13
|
+
* which is why the status-like fields are open sets.
|
|
14
|
+
*
|
|
15
|
+
* A row is one process and is matched by `instance` and `pid`, never by `sid`:
|
|
16
|
+
* two processes may be running one session, and a launcher's process is here
|
|
17
|
+
* before it has a session at all. */
|
|
14
18
|
export const AgentInfo = Type.Object(
|
|
15
19
|
{
|
|
16
|
-
|
|
20
|
+
/** The session this process is running. Absent on a process a launcher
|
|
21
|
+
* started that the harness has not yet named a session for — it has a
|
|
22
|
+
* terminal and a start, and nothing to attach them to yet. */
|
|
23
|
+
sid: Type.Optional(Sid),
|
|
17
24
|
/** The instance that polled it, and whose host the pid belongs to. */
|
|
18
25
|
instance: InstanceId,
|
|
19
26
|
pid: Type.Integer({ minimum: 1 }),
|
|
@@ -39,7 +46,7 @@ export const AgentInfo = Type.Object(
|
|
|
39
46
|
* into. Absent when the process does not name one or its environment could
|
|
40
47
|
* not be read. Read from the running process rather than remembered from
|
|
41
48
|
* when it started, since resuming a session gives it a new process. */
|
|
42
|
-
terminal_id: Type.Optional(
|
|
49
|
+
terminal_id: Type.Optional(TerminalId),
|
|
43
50
|
/** Which namespace that terminal lives in. Absent means the process set
|
|
44
51
|
* none, which the multiplexer treats as its default — not the instance's
|
|
45
52
|
* own namespace, which can differ. Typing into the wrong namespace reports a
|
|
@@ -50,13 +57,15 @@ export const AgentInfo = Type.Object(
|
|
|
50
57
|
);
|
|
51
58
|
export type AgentInfo = Static<typeof AgentInfo>;
|
|
52
59
|
|
|
53
|
-
/** A row that is gone: the
|
|
54
|
-
*
|
|
55
|
-
*
|
|
60
|
+
/** A row that is gone: the process ended, or the instance that polled it
|
|
61
|
+
* stopped. Marked rather than absent, since a frame carries only what changed.
|
|
62
|
+
*
|
|
63
|
+
* It names the pid and not the session: the session may well still be there,
|
|
64
|
+
* with another process running it. */
|
|
56
65
|
export const AgentRemoved = Type.Object(
|
|
57
66
|
{
|
|
58
|
-
sid: Sid,
|
|
59
67
|
instance: InstanceId,
|
|
68
|
+
pid: Type.Integer({ minimum: 1 }),
|
|
60
69
|
removed: Type.Literal(true),
|
|
61
70
|
},
|
|
62
71
|
{ $id: "AgentRemoved" },
|
|
@@ -67,7 +76,7 @@ export const AgentElement = Type.Union([AgentInfo, AgentRemoved], { $id: "AgentE
|
|
|
67
76
|
export type AgentElement = Static<typeof AgentElement>;
|
|
68
77
|
|
|
69
78
|
/** The `agents` topic. Elements, like `peers`: the rows that changed since the
|
|
70
|
-
* last frame, matched by their `instance` and `
|
|
79
|
+
* last frame, matched by their `instance` and `pid`.
|
|
71
80
|
*
|
|
72
81
|
* The instance polls the harness only while somebody is listening here, so the
|
|
73
82
|
* list is as fresh as the subscription is old — and a poll that finds one
|