borgmcp-shared 0.6.2 → 0.6.4
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/README.md +23 -9
- package/dist/conformance/adapter.d.ts +27 -1
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +184 -17
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +139 -1
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +193 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +62 -2
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +177 -5
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/errors.d.ts +2 -0
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +2 -0
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/templates.d.ts +12 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +16 -3
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +18 -2
- package/docs/enrollment.md +97 -25
- package/docs/releasing.md +24 -22
- package/package.json +1 -1
- package/src/conformance/adapter.ts +398 -22
- package/src/conformance/index.ts +271 -1
- package/src/protocol/contract.ts +254 -11
- package/src/protocol/errors.ts +2 -0
- package/src/protocol/version.ts +2 -2
- package/src/templates.ts +20 -3
package/src/conformance/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { BroadcastHwm } from '../log-stream-hwm.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AttachResponse,
|
|
4
|
+
AssociateRepositoryCubeRequest,
|
|
5
|
+
CreateCubeRequest,
|
|
6
|
+
EnrollmentExchangeRequest,
|
|
7
|
+
ResolveRepositoryCubeRequest,
|
|
8
|
+
} from '../protocol/contract.js';
|
|
3
9
|
|
|
4
10
|
export * from './adapter.js';
|
|
5
11
|
|
|
@@ -167,6 +173,270 @@ export const ENROLLMENT_RETRY_CONFORMANCE: readonly EnrollmentRetryConformanceVe
|
|
|
167
173
|
},
|
|
168
174
|
];
|
|
169
175
|
|
|
176
|
+
export interface CreateCubeRetryConformanceVector {
|
|
177
|
+
name: string;
|
|
178
|
+
initial: CreateCubeRequest;
|
|
179
|
+
retry: CreateCubeRequest;
|
|
180
|
+
expected:
|
|
181
|
+
| { outcome: 'resolved_response'; status: 201 }
|
|
182
|
+
| { outcome: 'retry_tuple_mismatch'; status: 409; error: 'INVALID_INPUT' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const CREATE_CUBE_RETRY_KEY = '00000000-0000-4000-8000-000000000121';
|
|
186
|
+
const CREATE_CUBE_INITIAL: CreateCubeRequest = {
|
|
187
|
+
retry_key: CREATE_CUBE_RETRY_KEY,
|
|
188
|
+
name: 'Repository One',
|
|
189
|
+
working_repo_name: 'repository-one',
|
|
190
|
+
repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-one' },
|
|
191
|
+
template: 'default',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Stateful vectors: name, repository identity, and template bind the retry key. */
|
|
195
|
+
export const CREATE_CUBE_RETRY_CONFORMANCE: readonly CreateCubeRetryConformanceVector[] = [
|
|
196
|
+
{
|
|
197
|
+
name: 'exact retry resolves the authoritative response',
|
|
198
|
+
initial: CREATE_CUBE_INITIAL,
|
|
199
|
+
retry: CREATE_CUBE_INITIAL,
|
|
200
|
+
expected: { outcome: 'resolved_response', status: 201 },
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'changed repository display metadata resolves stored authoritative display',
|
|
204
|
+
initial: CREATE_CUBE_INITIAL,
|
|
205
|
+
retry: { ...CREATE_CUBE_INITIAL, working_repo_name: 'repository-one-renamed' },
|
|
206
|
+
expected: { outcome: 'resolved_response', status: 201 },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'cube-name mismatch is rejected',
|
|
210
|
+
initial: CREATE_CUBE_INITIAL,
|
|
211
|
+
retry: { ...CREATE_CUBE_INITIAL, name: 'Repository One Renamed' },
|
|
212
|
+
expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'template mismatch is rejected',
|
|
216
|
+
initial: CREATE_CUBE_INITIAL,
|
|
217
|
+
retry: { ...CREATE_CUBE_INITIAL, template: 'software-dev' },
|
|
218
|
+
expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'repository kind mismatch is rejected',
|
|
222
|
+
initial: CREATE_CUBE_INITIAL,
|
|
223
|
+
retry: {
|
|
224
|
+
...CREATE_CUBE_INITIAL,
|
|
225
|
+
repository: { kind: 'local', value: '00000000-0000-4000-8000-000000000122' },
|
|
226
|
+
},
|
|
227
|
+
expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'repository value mismatch is rejected',
|
|
231
|
+
initial: CREATE_CUBE_INITIAL,
|
|
232
|
+
retry: {
|
|
233
|
+
...CREATE_CUBE_INITIAL,
|
|
234
|
+
repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-two' },
|
|
235
|
+
},
|
|
236
|
+
expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
export interface CreateCubeAssociationConformanceVector {
|
|
241
|
+
name: string;
|
|
242
|
+
created: CreateCubeRequest;
|
|
243
|
+
request: CreateCubeRequest;
|
|
244
|
+
expected:
|
|
245
|
+
| { outcome: 'resolved'; authority_state_delta: Record<string, never> }
|
|
246
|
+
| {
|
|
247
|
+
outcome: 'created';
|
|
248
|
+
authority_state_delta: {
|
|
249
|
+
cubes: 1;
|
|
250
|
+
roles: 2;
|
|
251
|
+
grants: 1;
|
|
252
|
+
cube_create_bindings: 1;
|
|
253
|
+
repository_associations: 1;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A creator-scoped repository association resolves independently of operation retry keys. */
|
|
259
|
+
export const CREATE_CUBE_ASSOCIATION_CONFORMANCE:
|
|
260
|
+
readonly CreateCubeAssociationConformanceVector[] = [
|
|
261
|
+
{
|
|
262
|
+
name: 'fresh retry for the same repository resolves stored authoritative fields',
|
|
263
|
+
created: CREATE_CUBE_INITIAL,
|
|
264
|
+
request: {
|
|
265
|
+
...CREATE_CUBE_INITIAL,
|
|
266
|
+
retry_key: '00000000-0000-4000-8000-000000000123',
|
|
267
|
+
name: 'Ignored New Cube Name',
|
|
268
|
+
working_repo_name: 'ignored-new-display',
|
|
269
|
+
template: 'starter',
|
|
270
|
+
},
|
|
271
|
+
expected: { outcome: 'resolved', authority_state_delta: {} },
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'fresh retry for a different repository may create',
|
|
275
|
+
created: CREATE_CUBE_INITIAL,
|
|
276
|
+
request: {
|
|
277
|
+
...CREATE_CUBE_INITIAL,
|
|
278
|
+
retry_key: '00000000-0000-4000-8000-000000000124',
|
|
279
|
+
name: 'Repository Two',
|
|
280
|
+
working_repo_name: 'repository-two',
|
|
281
|
+
repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-two' },
|
|
282
|
+
},
|
|
283
|
+
expected: {
|
|
284
|
+
outcome: 'created',
|
|
285
|
+
authority_state_delta: {
|
|
286
|
+
cubes: 1,
|
|
287
|
+
roles: 2,
|
|
288
|
+
grants: 1,
|
|
289
|
+
cube_create_bindings: 1,
|
|
290
|
+
repository_associations: 1,
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
export interface ResolveRepositoryCubeConformanceVector {
|
|
297
|
+
name: string;
|
|
298
|
+
request: ResolveRepositoryCubeRequest;
|
|
299
|
+
associated: boolean;
|
|
300
|
+
expected:
|
|
301
|
+
| { outcome: 'none'; status: 200; authority_state_delta: Record<string, never> }
|
|
302
|
+
| { outcome: 'resolved'; status: 200; authority_state_delta: Record<string, never> };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const REPOSITORY_CUBE_ONE = '00000000-0000-4000-8000-000000000131';
|
|
306
|
+
const REPOSITORY_ONE: ResolveRepositoryCubeRequest = {
|
|
307
|
+
working_repo_name: 'repository-one',
|
|
308
|
+
repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-one' },
|
|
309
|
+
};
|
|
310
|
+
const REPOSITORY_TWO: ResolveRepositoryCubeRequest = {
|
|
311
|
+
working_repo_name: 'repository-two',
|
|
312
|
+
repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-two' },
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** Resolution is read-only and returns only explicit none or authoritative stored fields. */
|
|
316
|
+
export const RESOLVE_REPOSITORY_CUBE_CONFORMANCE:
|
|
317
|
+
readonly ResolveRepositoryCubeConformanceVector[] = [
|
|
318
|
+
{
|
|
319
|
+
name: 'unassociated repository resolves explicit none without mutation',
|
|
320
|
+
request: REPOSITORY_ONE,
|
|
321
|
+
associated: false,
|
|
322
|
+
expected: { outcome: 'none', status: 200, authority_state_delta: {} },
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'associated repository resolves authoritative stored fields without mutation',
|
|
326
|
+
request: { ...REPOSITORY_ONE, working_repo_name: 'ignored-new-display' },
|
|
327
|
+
associated: true,
|
|
328
|
+
expected: { outcome: 'resolved', status: 200, authority_state_delta: {} },
|
|
329
|
+
},
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
export interface AssociateRepositoryCubeConformanceVector {
|
|
333
|
+
name: string;
|
|
334
|
+
initial: AssociateRepositoryCubeRequest;
|
|
335
|
+
retry: AssociateRepositoryCubeRequest;
|
|
336
|
+
expected:
|
|
337
|
+
| {
|
|
338
|
+
outcome: 'resolved';
|
|
339
|
+
status: 200;
|
|
340
|
+
initial_authority_state_delta: { repository_associations: 1 };
|
|
341
|
+
retry_authority_state_delta: Record<string, never>;
|
|
342
|
+
}
|
|
343
|
+
| {
|
|
344
|
+
outcome: 'repository_conflict' | 'cube_conflict';
|
|
345
|
+
status: 409;
|
|
346
|
+
error: 'REPOSITORY_ALREADY_ASSOCIATED' | 'CUBE_ALREADY_ASSOCIATED';
|
|
347
|
+
diagnostic_disclosure: 'none';
|
|
348
|
+
retry_authority_state_delta: Record<string, never>;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Explicit cube IDs bind atomically; names never select or identify a cube. */
|
|
353
|
+
export const ASSOCIATE_REPOSITORY_CUBE_CONFORMANCE:
|
|
354
|
+
readonly AssociateRepositoryCubeConformanceVector[] = [
|
|
355
|
+
{
|
|
356
|
+
name: 'same explicit cube and repository binding is idempotent',
|
|
357
|
+
initial: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
358
|
+
retry: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
359
|
+
expected: {
|
|
360
|
+
outcome: 'resolved',
|
|
361
|
+
status: 200,
|
|
362
|
+
initial_authority_state_delta: { repository_associations: 1 },
|
|
363
|
+
retry_authority_state_delta: {},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'repository already bound to another cube conflicts without mutation',
|
|
368
|
+
initial: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
369
|
+
retry: { cube_id: '00000000-0000-4000-8000-000000000132', ...REPOSITORY_ONE },
|
|
370
|
+
expected: {
|
|
371
|
+
outcome: 'repository_conflict',
|
|
372
|
+
status: 409,
|
|
373
|
+
error: 'REPOSITORY_ALREADY_ASSOCIATED',
|
|
374
|
+
diagnostic_disclosure: 'none',
|
|
375
|
+
retry_authority_state_delta: {},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: 'cube already bound to another repository conflicts without mutation',
|
|
380
|
+
initial: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
381
|
+
retry: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_TWO },
|
|
382
|
+
expected: {
|
|
383
|
+
outcome: 'cube_conflict',
|
|
384
|
+
status: 409,
|
|
385
|
+
error: 'CUBE_ALREADY_ASSOCIATED',
|
|
386
|
+
diagnostic_disclosure: 'none',
|
|
387
|
+
retry_authority_state_delta: {},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
export const REPOSITORY_CUBE_PERMISSION_CONFORMANCE = [
|
|
393
|
+
{
|
|
394
|
+
name: 'association denies an inaccessible explicit cube without mutation',
|
|
395
|
+
request: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
396
|
+
expected: { status: 403, error: 'ACCESS_DENIED', authority_state_delta: {} },
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: 'same-client binding to an inaccessible cube is non-enumerating',
|
|
400
|
+
request: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
401
|
+
precondition: 'repository_bound_to_inaccessible_cube',
|
|
402
|
+
expected: {
|
|
403
|
+
resolve: { status: 200, outcome: 'none', authority_state_delta: {} },
|
|
404
|
+
associate: {
|
|
405
|
+
status: 403,
|
|
406
|
+
error: 'ACCESS_DENIED',
|
|
407
|
+
diagnostic_disclosure: 'none',
|
|
408
|
+
authority_state_delta: {},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: 'another client binding is neither resolved nor treated as a conflict',
|
|
414
|
+
request: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
415
|
+
precondition: 'repository_bound_by_another_client',
|
|
416
|
+
expected: {
|
|
417
|
+
resolve: { status: 200, outcome: 'none', authority_state_delta: {} },
|
|
418
|
+
associate: {
|
|
419
|
+
status: 200,
|
|
420
|
+
outcome: 'resolved',
|
|
421
|
+
authority_state_delta: { repository_associations: 1 },
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
] as const;
|
|
426
|
+
|
|
427
|
+
export const REPOSITORY_CUBE_AUTHORITATIVE_STATE_CONFORMANCE = [
|
|
428
|
+
{
|
|
429
|
+
name: 'legacy cube with invalid authoritative roles is rejected without mutation',
|
|
430
|
+
request: { cube_id: REPOSITORY_CUBE_ONE, ...REPOSITORY_ONE },
|
|
431
|
+
expected: {
|
|
432
|
+
status: 409,
|
|
433
|
+
error: 'INVALID_INPUT',
|
|
434
|
+
diagnostic_disclosure: 'none',
|
|
435
|
+
authority_state_delta: {},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
] as const;
|
|
439
|
+
|
|
170
440
|
export const ENROLLMENT_AUTHORITY_CONFORMANCE = [
|
|
171
441
|
{
|
|
172
442
|
name: 'ordinary enrollment creates no authority or cube state',
|
package/src/protocol/contract.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ErrorCode } from './errors.js';
|
|
2
2
|
import { PROTOCOL_VERSION, type ProtocolVersion } from './version.js';
|
|
3
3
|
import {
|
|
4
|
+
canonicalizeRepositoryIdentity,
|
|
4
5
|
RuntimeMetadataValidationError,
|
|
5
6
|
validateRuntimeMetadata,
|
|
6
7
|
validateRuntimeMetadataPatch,
|
|
@@ -12,12 +13,14 @@ import type {
|
|
|
12
13
|
} from './types.js';
|
|
13
14
|
|
|
14
15
|
export const SHARED_PACKAGE_NAME = 'borgmcp-shared' as const;
|
|
15
|
-
export const SHARED_PACKAGE_VERSION = '0.6.
|
|
16
|
+
export const SHARED_PACKAGE_VERSION = '0.6.4' as const;
|
|
16
17
|
|
|
17
18
|
export const HEALTH_PATH = '/healthz' as const;
|
|
18
19
|
export const PROTOCOL_INFO_PATH = '/api/protocol' as const;
|
|
19
20
|
export const ENROLLMENT_EXCHANGE_PATH = '/api/enrollment/exchange' as const;
|
|
20
21
|
export const CUBES_PATH = '/api/cubes' as const;
|
|
22
|
+
export const REPOSITORY_CUBE_RESOLVE_PATH = '/api/repository-cubes/resolve' as const;
|
|
23
|
+
export const REPOSITORY_CUBE_ASSOCIATION_PATH = '/api/repository-cubes/association' as const;
|
|
21
24
|
export const ATTACH_PATH = '/api/client/attach' as const;
|
|
22
25
|
export const SELF_RUNTIME_METADATA_PATH = '/api/cubes/:cubeId/drones/self/metadata' as const;
|
|
23
26
|
|
|
@@ -26,6 +29,20 @@ export const PROTOCOL_HTTP_CONTRACT = {
|
|
|
26
29
|
protocol: { method: 'GET', path: PROTOCOL_INFO_PATH, authenticated: false, success_status: 200 },
|
|
27
30
|
enrollment: { method: 'POST', path: ENROLLMENT_EXCHANGE_PATH, authenticated: 'invitation', success_status: 201 },
|
|
28
31
|
cubes: { method: 'POST', path: CUBES_PATH, authenticated: true, success_status: 201 },
|
|
32
|
+
repository_cube_resolve: {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
path: REPOSITORY_CUBE_RESOLVE_PATH,
|
|
35
|
+
authenticated: true,
|
|
36
|
+
success_status: 200,
|
|
37
|
+
mutation: false,
|
|
38
|
+
},
|
|
39
|
+
repository_cube_association: {
|
|
40
|
+
method: 'PUT',
|
|
41
|
+
path: REPOSITORY_CUBE_ASSOCIATION_PATH,
|
|
42
|
+
authenticated: true,
|
|
43
|
+
success_status: 200,
|
|
44
|
+
mutation: true,
|
|
45
|
+
},
|
|
29
46
|
attach: { method: 'POST', path: ATTACH_PATH, authenticated: true, success_status: 200 },
|
|
30
47
|
drone_reassign: {
|
|
31
48
|
method: 'PATCH',
|
|
@@ -120,22 +137,60 @@ export type EnrollmentExchangeResponse =
|
|
|
120
137
|
| ClientEnrollmentExchangeResponse
|
|
121
138
|
| OwnerEnrollmentExchangeResponse;
|
|
122
139
|
|
|
123
|
-
export const CUBE_TEMPLATES = ['default'] as const;
|
|
140
|
+
export const CUBE_TEMPLATES = ['default', 'software-dev', 'starter'] as const;
|
|
124
141
|
export type CubeTemplate = (typeof CUBE_TEMPLATES)[number];
|
|
125
142
|
|
|
143
|
+
export type CreateCubeRepository =
|
|
144
|
+
| { kind: 'origin'; value: string }
|
|
145
|
+
| { kind: 'local'; value: string };
|
|
146
|
+
|
|
126
147
|
export interface CreateCubeRequest {
|
|
127
148
|
retry_key: string;
|
|
128
149
|
name: string;
|
|
150
|
+
working_repo_name: string;
|
|
151
|
+
repository: CreateCubeRepository;
|
|
129
152
|
template: CubeTemplate;
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
export interface CreateCubeResponse {
|
|
156
|
+
result: 'created' | 'resolved';
|
|
157
|
+
cube_id: string;
|
|
158
|
+
name: string;
|
|
159
|
+
working_repo_name: string;
|
|
160
|
+
repository: CreateCubeRepository;
|
|
161
|
+
template: CubeTemplate;
|
|
162
|
+
human_seat_role_id: string;
|
|
163
|
+
default_worker_role_id: string;
|
|
164
|
+
access: 'manage';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ResolveRepositoryCubeRequest {
|
|
168
|
+
working_repo_name: string;
|
|
169
|
+
repository: CreateCubeRepository;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface AssociateRepositoryCubeRequest extends ResolveRepositoryCubeRequest {
|
|
173
|
+
cube_id: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ResolvedRepositoryCube {
|
|
177
|
+
result: 'resolved';
|
|
133
178
|
cube_id: string;
|
|
179
|
+
name: string;
|
|
180
|
+
working_repo_name: string;
|
|
181
|
+
repository: CreateCubeRepository;
|
|
182
|
+
template: CubeTemplate;
|
|
134
183
|
human_seat_role_id: string;
|
|
135
184
|
default_worker_role_id: string;
|
|
136
185
|
access: 'manage';
|
|
137
186
|
}
|
|
138
187
|
|
|
188
|
+
export type ResolveRepositoryCubeResponse =
|
|
189
|
+
| { result: 'none' }
|
|
190
|
+
| ResolvedRepositoryCube;
|
|
191
|
+
|
|
192
|
+
export type AssociateRepositoryCubeResponse = ResolvedRepositoryCube;
|
|
193
|
+
|
|
139
194
|
export interface AckLogRequest {
|
|
140
195
|
entry_id: string;
|
|
141
196
|
kind: 'ack' | 'claim';
|
|
@@ -434,17 +489,24 @@ function decodeExactServerCapabilities(
|
|
|
434
489
|
|
|
435
490
|
export function decodeCreateCubeRequest(value: unknown): CreateCubeRequest {
|
|
436
491
|
const input = record(value);
|
|
437
|
-
exactKeys(
|
|
492
|
+
exactKeys(
|
|
493
|
+
input,
|
|
494
|
+
['retry_key', 'name', 'working_repo_name', 'repository', 'template'],
|
|
495
|
+
['retry_key', 'name', 'working_repo_name', 'repository', 'template'],
|
|
496
|
+
);
|
|
438
497
|
const name = boundedString(input.name, 1, 120, ['name']);
|
|
439
498
|
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(name)) {
|
|
440
499
|
fail('Cube name contains unsupported characters.', ['name']);
|
|
441
500
|
}
|
|
501
|
+
const workingRepoName = decodeWorkingRepositoryName(input.working_repo_name, ['working_repo_name']);
|
|
442
502
|
if (!CUBE_TEMPLATES.includes(input.template as CubeTemplate)) {
|
|
443
503
|
fail('Unsupported cube template.', ['template']);
|
|
444
504
|
}
|
|
445
505
|
return {
|
|
446
506
|
retry_key: decodeUuid(input.retry_key, ['retry_key']),
|
|
447
507
|
name,
|
|
508
|
+
working_repo_name: workingRepoName,
|
|
509
|
+
repository: decodeCreateCubeRepository(input.repository, ['repository']),
|
|
448
510
|
template: input.template as CubeTemplate,
|
|
449
511
|
};
|
|
450
512
|
}
|
|
@@ -457,22 +519,203 @@ export function decodeCreateCubeResponse(value: unknown): CreateCubeResponse {
|
|
|
457
519
|
const input = record(value);
|
|
458
520
|
exactKeys(
|
|
459
521
|
input,
|
|
460
|
-
[
|
|
461
|
-
|
|
522
|
+
[
|
|
523
|
+
'result',
|
|
524
|
+
'cube_id',
|
|
525
|
+
'name',
|
|
526
|
+
'working_repo_name',
|
|
527
|
+
'repository',
|
|
528
|
+
'template',
|
|
529
|
+
'human_seat_role_id',
|
|
530
|
+
'default_worker_role_id',
|
|
531
|
+
'access',
|
|
532
|
+
],
|
|
533
|
+
[
|
|
534
|
+
'result',
|
|
535
|
+
'cube_id',
|
|
536
|
+
'name',
|
|
537
|
+
'working_repo_name',
|
|
538
|
+
'repository',
|
|
539
|
+
'template',
|
|
540
|
+
'human_seat_role_id',
|
|
541
|
+
'default_worker_role_id',
|
|
542
|
+
'access',
|
|
543
|
+
],
|
|
462
544
|
);
|
|
545
|
+
if (input.result !== 'created' && input.result !== 'resolved') {
|
|
546
|
+
fail('Invalid cube creation result.', ['result']);
|
|
547
|
+
}
|
|
548
|
+
const name = boundedString(input.name, 1, 120, ['name']);
|
|
549
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(name)) {
|
|
550
|
+
fail('Cube name contains unsupported characters.', ['name']);
|
|
551
|
+
}
|
|
552
|
+
const workingRepoName = decodeWorkingRepositoryName(input.working_repo_name, ['working_repo_name']);
|
|
553
|
+
if (!CUBE_TEMPLATES.includes(input.template as CubeTemplate)) {
|
|
554
|
+
fail('Unsupported cube template.', ['template']);
|
|
555
|
+
}
|
|
463
556
|
if (input.access !== 'manage') fail('Created cube access must be manage.', ['access']);
|
|
464
557
|
return {
|
|
558
|
+
result: input.result,
|
|
465
559
|
cube_id: decodeUuid(input.cube_id, ['cube_id']),
|
|
560
|
+
name,
|
|
561
|
+
working_repo_name: workingRepoName,
|
|
562
|
+
repository: decodeCreateCubeRepository(input.repository, ['repository']),
|
|
563
|
+
template: input.template as CubeTemplate,
|
|
466
564
|
human_seat_role_id: decodeUuid(input.human_seat_role_id, ['human_seat_role_id']),
|
|
467
565
|
default_worker_role_id: decodeUuid(input.default_worker_role_id, ['default_worker_role_id']),
|
|
468
566
|
access: 'manage',
|
|
469
567
|
};
|
|
470
568
|
}
|
|
471
569
|
|
|
570
|
+
function decodeCreateCubeRepository(
|
|
571
|
+
value: unknown,
|
|
572
|
+
path: readonly (string | number)[],
|
|
573
|
+
): CreateCubeRepository {
|
|
574
|
+
const input = record(value, path);
|
|
575
|
+
exactKeys(input, ['kind', 'value'], ['kind', 'value'], path);
|
|
576
|
+
if (input.kind === 'local') {
|
|
577
|
+
return { kind: 'local', value: decodeUuid(input.value, [...path, 'value']) };
|
|
578
|
+
}
|
|
579
|
+
if (input.kind !== 'origin') fail('Unsupported repository identity kind.', [...path, 'kind']);
|
|
580
|
+
const origin = boundedString(input.value, 1, 512, [...path, 'value']);
|
|
581
|
+
try {
|
|
582
|
+
if (canonicalizeRepositoryIdentity(origin).working_repo_origin !== origin) {
|
|
583
|
+
fail('Repository origin must be canonical.', [...path, 'value']);
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error instanceof ProtocolContractError) throw error;
|
|
587
|
+
if (error instanceof RuntimeMetadataValidationError) {
|
|
588
|
+
fail('Repository origin must be canonical.', [...path, 'value']);
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
return { kind: 'origin', value: origin };
|
|
593
|
+
}
|
|
594
|
+
|
|
472
595
|
export function decodeCreateCubeResponseEnvelope(value: unknown): ProtocolEnvelope<CreateCubeResponse> {
|
|
473
596
|
return decodeProtocolEnvelope(value, decodeCreateCubeResponse);
|
|
474
597
|
}
|
|
475
598
|
|
|
599
|
+
export function decodeResolveRepositoryCubeRequest(value: unknown): ResolveRepositoryCubeRequest {
|
|
600
|
+
const input = record(value);
|
|
601
|
+
exactKeys(input, ['working_repo_name', 'repository'], ['working_repo_name', 'repository']);
|
|
602
|
+
return {
|
|
603
|
+
working_repo_name: decodeWorkingRepositoryName(input.working_repo_name, ['working_repo_name']),
|
|
604
|
+
repository: decodeCreateCubeRepository(input.repository, ['repository']),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function decodeResolveRepositoryCubeRequestEnvelope(
|
|
609
|
+
value: unknown,
|
|
610
|
+
): ProtocolEnvelope<ResolveRepositoryCubeRequest> {
|
|
611
|
+
return decodeProtocolEnvelope(value, decodeResolveRepositoryCubeRequest);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function decodeAssociateRepositoryCubeRequest(value: unknown): AssociateRepositoryCubeRequest {
|
|
615
|
+
const input = record(value);
|
|
616
|
+
exactKeys(
|
|
617
|
+
input,
|
|
618
|
+
['cube_id', 'working_repo_name', 'repository'],
|
|
619
|
+
['cube_id', 'working_repo_name', 'repository'],
|
|
620
|
+
);
|
|
621
|
+
return {
|
|
622
|
+
cube_id: decodeUuid(input.cube_id, ['cube_id']),
|
|
623
|
+
working_repo_name: decodeWorkingRepositoryName(input.working_repo_name, ['working_repo_name']),
|
|
624
|
+
repository: decodeCreateCubeRepository(input.repository, ['repository']),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function decodeAssociateRepositoryCubeRequestEnvelope(
|
|
629
|
+
value: unknown,
|
|
630
|
+
): ProtocolEnvelope<AssociateRepositoryCubeRequest> {
|
|
631
|
+
return decodeProtocolEnvelope(value, decodeAssociateRepositoryCubeRequest);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function decodeResolvedRepositoryCube(value: unknown): ResolvedRepositoryCube {
|
|
635
|
+
const input = record(value);
|
|
636
|
+
exactKeys(
|
|
637
|
+
input,
|
|
638
|
+
[
|
|
639
|
+
'result',
|
|
640
|
+
'cube_id',
|
|
641
|
+
'name',
|
|
642
|
+
'working_repo_name',
|
|
643
|
+
'repository',
|
|
644
|
+
'template',
|
|
645
|
+
'human_seat_role_id',
|
|
646
|
+
'default_worker_role_id',
|
|
647
|
+
'access',
|
|
648
|
+
],
|
|
649
|
+
[
|
|
650
|
+
'result',
|
|
651
|
+
'cube_id',
|
|
652
|
+
'name',
|
|
653
|
+
'working_repo_name',
|
|
654
|
+
'repository',
|
|
655
|
+
'template',
|
|
656
|
+
'human_seat_role_id',
|
|
657
|
+
'default_worker_role_id',
|
|
658
|
+
'access',
|
|
659
|
+
],
|
|
660
|
+
);
|
|
661
|
+
if (input.result !== 'resolved') fail('Invalid repository cube result.', ['result']);
|
|
662
|
+
const name = boundedString(input.name, 1, 120, ['name']);
|
|
663
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(name)) {
|
|
664
|
+
fail('Cube name contains unsupported characters.', ['name']);
|
|
665
|
+
}
|
|
666
|
+
if (!CUBE_TEMPLATES.includes(input.template as CubeTemplate)) {
|
|
667
|
+
fail('Unsupported cube template.', ['template']);
|
|
668
|
+
}
|
|
669
|
+
if (input.access !== 'manage') fail('Repository cube access must be manage.', ['access']);
|
|
670
|
+
return {
|
|
671
|
+
result: 'resolved',
|
|
672
|
+
cube_id: decodeUuid(input.cube_id, ['cube_id']),
|
|
673
|
+
name,
|
|
674
|
+
working_repo_name: decodeWorkingRepositoryName(input.working_repo_name, ['working_repo_name']),
|
|
675
|
+
repository: decodeCreateCubeRepository(input.repository, ['repository']),
|
|
676
|
+
template: input.template as CubeTemplate,
|
|
677
|
+
human_seat_role_id: decodeUuid(input.human_seat_role_id, ['human_seat_role_id']),
|
|
678
|
+
default_worker_role_id: decodeUuid(input.default_worker_role_id, ['default_worker_role_id']),
|
|
679
|
+
access: 'manage',
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function decodeResolveRepositoryCubeResponse(value: unknown): ResolveRepositoryCubeResponse {
|
|
684
|
+
const input = record(value);
|
|
685
|
+
if (input.result === 'none') {
|
|
686
|
+
exactKeys(input, ['result'], ['result']);
|
|
687
|
+
return { result: 'none' };
|
|
688
|
+
}
|
|
689
|
+
return decodeResolvedRepositoryCube(input);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function decodeResolveRepositoryCubeResponseEnvelope(
|
|
693
|
+
value: unknown,
|
|
694
|
+
): ProtocolEnvelope<ResolveRepositoryCubeResponse> {
|
|
695
|
+
return decodeProtocolEnvelope(value, decodeResolveRepositoryCubeResponse);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function decodeAssociateRepositoryCubeResponse(value: unknown): AssociateRepositoryCubeResponse {
|
|
699
|
+
return decodeResolvedRepositoryCube(value);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function decodeAssociateRepositoryCubeResponseEnvelope(
|
|
703
|
+
value: unknown,
|
|
704
|
+
): ProtocolEnvelope<AssociateRepositoryCubeResponse> {
|
|
705
|
+
return decodeProtocolEnvelope(value, decodeAssociateRepositoryCubeResponse);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function decodeWorkingRepositoryName(
|
|
709
|
+
value: unknown,
|
|
710
|
+
path: readonly (string | number)[],
|
|
711
|
+
): string {
|
|
712
|
+
const name = boundedString(value, 1, 120, path);
|
|
713
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(name)) {
|
|
714
|
+
fail('Repository name contains unsupported characters.', path);
|
|
715
|
+
}
|
|
716
|
+
return name;
|
|
717
|
+
}
|
|
718
|
+
|
|
476
719
|
export function decodeAppendLogRequest(value: unknown): import('./types.js').AppendLogRequest {
|
|
477
720
|
const input = record(value);
|
|
478
721
|
exactKeys(
|
|
@@ -698,7 +941,7 @@ export function decodeUpdateDroneRuntimeMetadataResponseEnvelope(
|
|
|
698
941
|
return decodeProtocolEnvelope(value, decodeUpdateDroneRuntimeMetadataResponse);
|
|
699
942
|
}
|
|
700
943
|
|
|
701
|
-
// ──
|
|
944
|
+
// ── Clean-slate attach wire types (introduced in v3) ──────────────────────
|
|
702
945
|
|
|
703
946
|
export interface AttachRequest {
|
|
704
947
|
cube_id: string;
|
|
@@ -800,7 +1043,7 @@ function decodeAttachSession(value: unknown, path: readonly (string | number)[])
|
|
|
800
1043
|
}
|
|
801
1044
|
|
|
802
1045
|
/**
|
|
803
|
-
* Decode
|
|
1046
|
+
* Decode an attach request. Strict: exact keys, bounded sizes,
|
|
804
1047
|
* session_credential is token-safe and never echoed in errors.
|
|
805
1048
|
*/
|
|
806
1049
|
export function decodeAttachRequest(value: unknown): AttachRequest {
|
|
@@ -825,7 +1068,7 @@ export function decodeAttachRequest(value: unknown): AttachRequest {
|
|
|
825
1068
|
}
|
|
826
1069
|
|
|
827
1070
|
/**
|
|
828
|
-
* Create
|
|
1071
|
+
* Create an attach request envelope. Stamps the canonical protocol version.
|
|
829
1072
|
*/
|
|
830
1073
|
export function createAttachRequestEnvelope(
|
|
831
1074
|
requestId: string,
|
|
@@ -839,7 +1082,7 @@ export function createAttachRequestEnvelope(
|
|
|
839
1082
|
}
|
|
840
1083
|
|
|
841
1084
|
/**
|
|
842
|
-
* Decode
|
|
1085
|
+
* Decode an attach request envelope. Verifies protocol_version === PROTOCOL_VERSION
|
|
843
1086
|
* BEFORE decoding the payload — a wrong tag never invokes the payload decoder
|
|
844
1087
|
* and never exposes or returns the supplied session_credential.
|
|
845
1088
|
* Uses a static token-safe diagnostic; does not interpolate attacker-controlled text.
|
|
@@ -869,7 +1112,7 @@ export function decodeAttachRequestEnvelope(
|
|
|
869
1112
|
}
|
|
870
1113
|
|
|
871
1114
|
/**
|
|
872
|
-
* Decode
|
|
1115
|
+
* Decode an attach response. Strict: exact keys and result discriminant.
|
|
873
1116
|
*/
|
|
874
1117
|
export function decodeAttachResponse(value: unknown): AttachResponse {
|
|
875
1118
|
const input = record(value);
|
|
@@ -893,7 +1136,7 @@ export function decodeAttachResponse(value: unknown): AttachResponse {
|
|
|
893
1136
|
}
|
|
894
1137
|
|
|
895
1138
|
/**
|
|
896
|
-
* Decode
|
|
1139
|
+
* Decode an attach response wrapped in a ProtocolEnvelope.
|
|
897
1140
|
* Verifies protocol_version === PROTOCOL_VERSION before decoding payload.
|
|
898
1141
|
*/
|
|
899
1142
|
export function decodeAttachResponseEnvelope(value: unknown): ProtocolEnvelope<AttachResponse> {
|
package/src/protocol/errors.ts
CHANGED
|
@@ -13,6 +13,8 @@ export enum ErrorCode {
|
|
|
13
13
|
DATABASE_ERROR = 'DATABASE_ERROR',
|
|
14
14
|
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
|
|
15
15
|
NOT_FOUND = 'NOT_FOUND',
|
|
16
|
+
REPOSITORY_ALREADY_ASSOCIATED = 'REPOSITORY_ALREADY_ASSOCIATED',
|
|
17
|
+
CUBE_ALREADY_ASSOCIATED = 'CUBE_ALREADY_ASSOCIATED',
|
|
16
18
|
ROLE_IN_USE = 'ROLE_IN_USE',
|
|
17
19
|
ROLE_HAS_FROZEN_DRONES = 'ROLE_HAS_FROZEN_DRONES',
|
|
18
20
|
DRONE_EVICTED = 'DRONE_EVICTED',
|
package/src/protocol/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Current Borg coordination protocol generation. Clean-slate
|
|
2
|
-
export const PROTOCOL_VERSION = '
|
|
1
|
+
/** Current Borg coordination protocol generation. Clean-slate v5. */
|
|
2
|
+
export const PROTOCOL_VERSION = '5' as const;
|
|
3
3
|
|
|
4
4
|
export type ProtocolVersion = typeof PROTOCOL_VERSION;
|