borgmcp-shared 0.6.4 → 0.7.1
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 +5 -23
- package/RELEASES.md +29 -0
- package/dist/conformance/adapter.d.ts +19 -0
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +141 -1
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +32 -1
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +167 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +20 -2
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +33 -3
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/coordination.d.ts +2 -2
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +36 -2
- package/dist/protocol/coordination.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -0
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +1 -0
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/sse.d.ts +6 -2
- package/dist/protocol/sse.d.ts.map +1 -1
- package/dist/protocol/sse.js +8 -2
- package/dist/protocol/sse.js.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/templates.d.ts +4 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +182 -1
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +16 -4
- package/docs/enrollment.md +14 -7
- package/docs/releasing.md +15 -9
- package/package.json +2 -1
- package/src/conformance/adapter.ts +294 -0
- package/src/conformance/index.ts +203 -0
- package/src/protocol/contract.ts +43 -3
- package/src/protocol/coordination.ts +53 -4
- package/src/protocol/errors.ts +1 -0
- package/src/protocol/sse.ts +17 -2
- package/src/protocol/version.ts +2 -2
- package/src/templates.ts +188 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createProtocolEnvelope,
|
|
5
5
|
decodeAssociateRepositoryCubeResponseEnvelope,
|
|
6
6
|
decodeCreateCubeResponseEnvelope,
|
|
7
|
+
decodeDeleteCubeResponseEnvelope,
|
|
7
8
|
decodeAppendLogResultEnvelope,
|
|
8
9
|
decodeDecisionResultEnvelope,
|
|
9
10
|
decodeDecisionsResultEnvelope,
|
|
@@ -130,6 +131,20 @@ export interface ConformanceCreatedCubeState {
|
|
|
130
131
|
default_worker_role_matches: boolean;
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
export interface ConformanceDeletedCubeState {
|
|
135
|
+
readonly cube_exists: boolean;
|
|
136
|
+
readonly role_count: number;
|
|
137
|
+
readonly drone_count: number;
|
|
138
|
+
readonly log_count: number;
|
|
139
|
+
readonly claim_count: number;
|
|
140
|
+
readonly decision_count: number;
|
|
141
|
+
readonly grant_count: number;
|
|
142
|
+
readonly cube_create_binding_count: number;
|
|
143
|
+
readonly repository_association_count: number;
|
|
144
|
+
readonly active_stream_count: number;
|
|
145
|
+
readonly terminal_credential_count: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
export interface ConformanceRepositoryCubeFixture {
|
|
134
149
|
cube_id: string;
|
|
135
150
|
name: string;
|
|
@@ -168,6 +183,8 @@ export interface ConformanceStreamResponse extends ConformanceHttpResponse {
|
|
|
168
183
|
*/
|
|
169
184
|
export interface ConformanceAdmin {
|
|
170
185
|
reset(): Promise<void>;
|
|
186
|
+
/** Restarts the authority while preserving durable state. */
|
|
187
|
+
restartAuthority(): Promise<void>;
|
|
171
188
|
createPrincipal(name: string): Promise<ConformancePrincipal>;
|
|
172
189
|
createCube(name: string): Promise<ConformanceCube>;
|
|
173
190
|
/** Grants the requested cube authority; omitted access defaults to manage. */
|
|
@@ -205,6 +222,7 @@ export interface ConformanceAdmin {
|
|
|
205
222
|
creator: ConformancePrincipal,
|
|
206
223
|
response: CreateCubeResponse,
|
|
207
224
|
): Promise<ConformanceCreatedCubeState>;
|
|
225
|
+
inspectDeletedCube(cube: ConformanceCube): Promise<ConformanceDeletedCubeState>;
|
|
208
226
|
prepareRepositoryCube(
|
|
209
227
|
cube: ConformanceCube,
|
|
210
228
|
input: {
|
|
@@ -227,6 +245,11 @@ export interface ConformanceOperations {
|
|
|
227
245
|
protocol(credential: string | null): Promise<ConformanceHttpResponse>;
|
|
228
246
|
enroll(request: unknown): Promise<ConformanceHttpResponse>;
|
|
229
247
|
createCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
|
|
248
|
+
deleteCube(
|
|
249
|
+
credential: string,
|
|
250
|
+
cube: ConformanceCube,
|
|
251
|
+
request: unknown,
|
|
252
|
+
): Promise<ConformanceHttpResponse>;
|
|
230
253
|
resolveRepositoryCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
|
|
231
254
|
associateRepositoryCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
|
|
232
255
|
attach(credential: string, request: unknown): Promise<ConformanceHttpResponse>;
|
|
@@ -337,6 +360,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
337
360
|
{ id: 'security.metadata-noninterference', area: 'security' },
|
|
338
361
|
{ id: 'security.metadata-secret-non-echo', area: 'security' },
|
|
339
362
|
{ id: 'security.active-stream-revocation', area: 'security' },
|
|
363
|
+
{ id: 'cubes.delete-terminal-cascade', area: 'cubes' },
|
|
340
364
|
] as const;
|
|
341
365
|
|
|
342
366
|
export type AdapterConformanceFixtureId =
|
|
@@ -2431,6 +2455,276 @@ export async function runAdapterConformance(
|
|
|
2431
2455
|
return { stream_terminated: true, subsequent_status: 401, subsequent_code: ErrorCode.SESSION_REVOKED };
|
|
2432
2456
|
});
|
|
2433
2457
|
|
|
2458
|
+
await record('cubes.delete-terminal-cascade', async () => {
|
|
2459
|
+
const enrollParent = async (
|
|
2460
|
+
name: string,
|
|
2461
|
+
marker: string,
|
|
2462
|
+
retrySuffix: string,
|
|
2463
|
+
): Promise<{ principal: ConformancePrincipal; credential: string }> => {
|
|
2464
|
+
const principal = await environment.admin.createPrincipal(name);
|
|
2465
|
+
const invitation = await environment.admin.issueSingleUseInvitation(principal, 'client');
|
|
2466
|
+
const credential = `${marker.repeat(41)}AQ`;
|
|
2467
|
+
expectStatus(await environment.operations.enroll(createProtocolEnvelope(
|
|
2468
|
+
`delete-${name}-enroll`,
|
|
2469
|
+
{
|
|
2470
|
+
invitation,
|
|
2471
|
+
retry_key: `00000000-0000-4000-8000-${retrySuffix.padStart(12, '0')}`,
|
|
2472
|
+
client_credential: credential,
|
|
2473
|
+
client_name: name,
|
|
2474
|
+
},
|
|
2475
|
+
)), 201, `${name} enrollment`);
|
|
2476
|
+
return { principal, credential };
|
|
2477
|
+
};
|
|
2478
|
+
const creator = await enrollParent('delete-creator', 'C', '701');
|
|
2479
|
+
const manager = await enrollParent('delete-manager', 'M', '702');
|
|
2480
|
+
const reader = await enrollParent('delete-reader', 'R', '703');
|
|
2481
|
+
const writer = await enrollParent('delete-writer', 'W', '704');
|
|
2482
|
+
const outsider = await enrollParent('delete-outsider', 'O', '705');
|
|
2483
|
+
await environment.admin.grantCreateCubeCapability(creator.principal);
|
|
2484
|
+
const createResponse = await environment.operations.createCube(
|
|
2485
|
+
creator.credential,
|
|
2486
|
+
createProtocolEnvelope('delete-cube-create', {
|
|
2487
|
+
retry_key: '00000000-0000-4000-8000-000000000706',
|
|
2488
|
+
name: 'Disposable Cube',
|
|
2489
|
+
working_repo_name: 'disposable-cube',
|
|
2490
|
+
repository: { kind: 'local', value: '00000000-0000-4000-8000-000000000707' },
|
|
2491
|
+
template: 'default',
|
|
2492
|
+
}),
|
|
2493
|
+
);
|
|
2494
|
+
expectStatus(createResponse, 201, 'Disposable cube creation');
|
|
2495
|
+
const created = decodeCreateCubeResponseEnvelope(createResponse.body).payload;
|
|
2496
|
+
const cube = { id: created.cube_id };
|
|
2497
|
+
const role = { id: created.default_worker_role_id };
|
|
2498
|
+
await environment.admin.grantCube(manager.principal, cube, 'manage');
|
|
2499
|
+
await environment.admin.grantCube(reader.principal, cube, 'read');
|
|
2500
|
+
await environment.admin.grantCube(writer.principal, cube, 'write');
|
|
2501
|
+
const drone = await environment.admin.createDrone(creator.principal, cube, role);
|
|
2502
|
+
const droneCredential = await environment.admin.issueManagedDroneSession(drone);
|
|
2503
|
+
const append = await environment.operations.append(
|
|
2504
|
+
creator.credential,
|
|
2505
|
+
cube,
|
|
2506
|
+
createProtocolEnvelope('delete-log', { message: 'deleted with cube' }),
|
|
2507
|
+
);
|
|
2508
|
+
expectStatus(append, 201, 'Deletion fixture log');
|
|
2509
|
+
const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
|
|
2510
|
+
expectStatus(await environment.operations.ack(
|
|
2511
|
+
creator.credential,
|
|
2512
|
+
cube,
|
|
2513
|
+
createProtocolEnvelope('delete-claim', { entry_id: entry.id, kind: 'claim' }),
|
|
2514
|
+
), 204, 'Deletion fixture claim');
|
|
2515
|
+
expectStatus(await environment.operations.recordDecision(
|
|
2516
|
+
manager.credential,
|
|
2517
|
+
cube,
|
|
2518
|
+
createProtocolEnvelope('delete-decision', { topic: 'cleanup', decision: 'delete' }),
|
|
2519
|
+
), 201, 'Deletion fixture decision');
|
|
2520
|
+
|
|
2521
|
+
const beforeDenied = await environment.admin.inspectCubeManagementState(cube);
|
|
2522
|
+
for (const [kind, credential] of [
|
|
2523
|
+
['read', reader.credential],
|
|
2524
|
+
['write', writer.credential],
|
|
2525
|
+
['drone-session', droneCredential],
|
|
2526
|
+
] as const) {
|
|
2527
|
+
expectError(
|
|
2528
|
+
await environment.operations.deleteCube(
|
|
2529
|
+
credential,
|
|
2530
|
+
cube,
|
|
2531
|
+
createProtocolEnvelope(`delete-${kind}-denied`, {}),
|
|
2532
|
+
),
|
|
2533
|
+
403,
|
|
2534
|
+
ErrorCode.ACCESS_DENIED,
|
|
2535
|
+
`${kind} cube deletion`,
|
|
2536
|
+
);
|
|
2537
|
+
invariant(
|
|
2538
|
+
same(await environment.admin.inspectCubeManagementState(cube), beforeDenied),
|
|
2539
|
+
`${kind} cube deletion denial mutated the cube.`,
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
expectError(
|
|
2543
|
+
await environment.operations.deleteCube(
|
|
2544
|
+
outsider.credential,
|
|
2545
|
+
cube,
|
|
2546
|
+
createProtocolEnvelope('delete-outsider-denied', {}),
|
|
2547
|
+
),
|
|
2548
|
+
404,
|
|
2549
|
+
ErrorCode.NOT_FOUND,
|
|
2550
|
+
'Never-authorized cube deletion',
|
|
2551
|
+
);
|
|
2552
|
+
|
|
2553
|
+
const parentStream = await environment.operations.openStream(manager.credential, cube, null);
|
|
2554
|
+
const droneStream = await environment.operations.openStream(droneCredential, cube, null);
|
|
2555
|
+
expectStatus(parentStream, 200, 'Deletion parent stream');
|
|
2556
|
+
expectStatus(droneStream, 200, 'Deletion drone stream');
|
|
2557
|
+
invariant(parentStream.stream && droneStream.stream, 'Deletion fixture streams did not open.');
|
|
2558
|
+
const parentEvents = new SseEventReader(parentStream.stream);
|
|
2559
|
+
const droneEvents = new SseEventReader(droneStream.stream);
|
|
2560
|
+
for (const streamReader of [parentEvents, droneEvents]) {
|
|
2561
|
+
invariant((await streamReader.next()).type === 'log', 'Deletion stream omitted replay log.');
|
|
2562
|
+
const bookmark = await streamReader.next();
|
|
2563
|
+
invariant(bookmark.type === 'bookmark' && bookmark.replay_complete, 'Deletion stream omitted bookmark.');
|
|
2564
|
+
}
|
|
2565
|
+
const parentTerminal = parentEvents.next();
|
|
2566
|
+
const droneTerminal = droneEvents.next();
|
|
2567
|
+
await provePending(parentTerminal, 'Parent deletion terminal event', pendingProbeMs);
|
|
2568
|
+
await provePending(droneTerminal, 'Drone deletion terminal event', pendingProbeMs);
|
|
2569
|
+
|
|
2570
|
+
const deleted = await environment.operations.deleteCube(
|
|
2571
|
+
manager.credential,
|
|
2572
|
+
cube,
|
|
2573
|
+
createProtocolEnvelope('delete-managed-cube', {}),
|
|
2574
|
+
);
|
|
2575
|
+
expectStatus(deleted, 200, 'Managed cube deletion');
|
|
2576
|
+
invariant(
|
|
2577
|
+
same(decodeDeleteCubeResponseEnvelope(deleted.body).payload, {
|
|
2578
|
+
cube_id: cube.id,
|
|
2579
|
+
deleted: true,
|
|
2580
|
+
}),
|
|
2581
|
+
'Cube deletion response did not identify the terminal cube.',
|
|
2582
|
+
);
|
|
2583
|
+
for (const [kind, event] of [
|
|
2584
|
+
['parent', await within(parentTerminal, 'Parent deletion terminal event', streamDeadlineMs)],
|
|
2585
|
+
['drone', await within(droneTerminal, 'Drone deletion terminal event', streamDeadlineMs)],
|
|
2586
|
+
] as const) {
|
|
2587
|
+
invariant(
|
|
2588
|
+
event.type === 'error' && event.error.error.code === ErrorCode.CUBE_DELETED,
|
|
2589
|
+
`${kind} stream did not receive CUBE_DELETED.`,
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
for (const streamReader of [parentEvents, droneEvents]) {
|
|
2593
|
+
let closed = false;
|
|
2594
|
+
try {
|
|
2595
|
+
await within(streamReader.next(), 'Deleted cube stream close', streamDeadlineMs);
|
|
2596
|
+
} catch (error) {
|
|
2597
|
+
if (error instanceof Error && error.message.includes('did not settle')) throw error;
|
|
2598
|
+
closed = true;
|
|
2599
|
+
}
|
|
2600
|
+
invariant(closed, 'Deleted cube stream yielded after its terminal error.');
|
|
2601
|
+
await streamReader.close();
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
invariant(
|
|
2605
|
+
same(await environment.admin.inspectDeletedCube(cube), {
|
|
2606
|
+
cube_exists: false,
|
|
2607
|
+
role_count: 0,
|
|
2608
|
+
drone_count: 0,
|
|
2609
|
+
log_count: 0,
|
|
2610
|
+
claim_count: 0,
|
|
2611
|
+
decision_count: 0,
|
|
2612
|
+
grant_count: 0,
|
|
2613
|
+
cube_create_binding_count: 0,
|
|
2614
|
+
repository_association_count: 0,
|
|
2615
|
+
active_stream_count: 0,
|
|
2616
|
+
terminal_credential_count: 5,
|
|
2617
|
+
}),
|
|
2618
|
+
'Cube deletion did not atomically remove cube-owned state and preserve only terminal credentials.',
|
|
2619
|
+
);
|
|
2620
|
+
for (const [kind, credential] of [
|
|
2621
|
+
['creator', creator.credential],
|
|
2622
|
+
['manager', manager.credential],
|
|
2623
|
+
['reader', reader.credential],
|
|
2624
|
+
['writer', writer.credential],
|
|
2625
|
+
['drone', droneCredential],
|
|
2626
|
+
] as const) {
|
|
2627
|
+
expectError(
|
|
2628
|
+
await environment.operations.read(
|
|
2629
|
+
credential,
|
|
2630
|
+
cube,
|
|
2631
|
+
createProtocolEnvelope(`delete-${kind}-terminal`, { cursor: null, limit: 1 }),
|
|
2632
|
+
),
|
|
2633
|
+
PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
|
|
2634
|
+
ErrorCode.CUBE_DELETED,
|
|
2635
|
+
`${kind} post-delete request`,
|
|
2636
|
+
);
|
|
2637
|
+
expectError(
|
|
2638
|
+
await environment.operations.deleteCube(
|
|
2639
|
+
credential,
|
|
2640
|
+
cube,
|
|
2641
|
+
createProtocolEnvelope(`delete-${kind}-repeat-terminal`, {}),
|
|
2642
|
+
),
|
|
2643
|
+
PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
|
|
2644
|
+
ErrorCode.CUBE_DELETED,
|
|
2645
|
+
`${kind} post-delete DELETE`,
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
expectError(
|
|
2649
|
+
await environment.operations.deleteCube(
|
|
2650
|
+
outsider.credential,
|
|
2651
|
+
cube,
|
|
2652
|
+
createProtocolEnvelope('delete-outsider-hidden-repeat', {}),
|
|
2653
|
+
),
|
|
2654
|
+
404,
|
|
2655
|
+
ErrorCode.NOT_FOUND,
|
|
2656
|
+
'Never-authorized post-delete DELETE',
|
|
2657
|
+
);
|
|
2658
|
+
expectError(
|
|
2659
|
+
await environment.operations.read(
|
|
2660
|
+
outsider.credential,
|
|
2661
|
+
cube,
|
|
2662
|
+
createProtocolEnvelope('delete-outsider-hidden', { cursor: null, limit: 1 }),
|
|
2663
|
+
),
|
|
2664
|
+
404,
|
|
2665
|
+
ErrorCode.NOT_FOUND,
|
|
2666
|
+
'Never-authorized post-delete request',
|
|
2667
|
+
);
|
|
2668
|
+
await environment.admin.restartAuthority();
|
|
2669
|
+
for (const [kind, credential] of [
|
|
2670
|
+
['creator', creator.credential],
|
|
2671
|
+
['manager', manager.credential],
|
|
2672
|
+
['reader', reader.credential],
|
|
2673
|
+
['writer', writer.credential],
|
|
2674
|
+
['drone', droneCredential],
|
|
2675
|
+
] as const) {
|
|
2676
|
+
expectError(
|
|
2677
|
+
await environment.operations.read(
|
|
2678
|
+
credential,
|
|
2679
|
+
cube,
|
|
2680
|
+
createProtocolEnvelope(`delete-${kind}-after-restart`, { cursor: null, limit: 1 }),
|
|
2681
|
+
),
|
|
2682
|
+
PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
|
|
2683
|
+
ErrorCode.CUBE_DELETED,
|
|
2684
|
+
`${kind} post-restart deleted-cube request`,
|
|
2685
|
+
);
|
|
2686
|
+
expectError(
|
|
2687
|
+
await environment.operations.deleteCube(
|
|
2688
|
+
credential,
|
|
2689
|
+
cube,
|
|
2690
|
+
createProtocolEnvelope(`delete-${kind}-repeat-after-restart`, {}),
|
|
2691
|
+
),
|
|
2692
|
+
PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
|
|
2693
|
+
ErrorCode.CUBE_DELETED,
|
|
2694
|
+
`${kind} post-restart DELETE`,
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
expectError(
|
|
2698
|
+
await environment.operations.deleteCube(
|
|
2699
|
+
outsider.credential,
|
|
2700
|
+
cube,
|
|
2701
|
+
createProtocolEnvelope('delete-outsider-hidden-repeat-after-restart', {}),
|
|
2702
|
+
),
|
|
2703
|
+
404,
|
|
2704
|
+
ErrorCode.NOT_FOUND,
|
|
2705
|
+
'Never-authorized post-restart DELETE',
|
|
2706
|
+
);
|
|
2707
|
+
expectError(
|
|
2708
|
+
await environment.operations.read(
|
|
2709
|
+
outsider.credential,
|
|
2710
|
+
cube,
|
|
2711
|
+
createProtocolEnvelope('delete-outsider-hidden-after-restart', { cursor: null, limit: 1 }),
|
|
2712
|
+
),
|
|
2713
|
+
404,
|
|
2714
|
+
ErrorCode.NOT_FOUND,
|
|
2715
|
+
'Never-authorized post-restart request',
|
|
2716
|
+
);
|
|
2717
|
+
return {
|
|
2718
|
+
deletion_status: 200,
|
|
2719
|
+
cascade_complete: true,
|
|
2720
|
+
terminal_stream_error: ErrorCode.CUBE_DELETED,
|
|
2721
|
+
terminal_http_status: 410,
|
|
2722
|
+
terminal_state_durable_after_restart: true,
|
|
2723
|
+
unknown_hidden_status: 404,
|
|
2724
|
+
non_member_manager: true,
|
|
2725
|
+
};
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2434
2728
|
const normalizedTranscript = results.map(({ id, observations }) => ({ id, observations }));
|
|
2435
2729
|
return { ok: results.every((result) => result.ok), results, normalizedTranscript };
|
|
2436
2730
|
}
|
package/src/conformance/index.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type {
|
|
|
3
3
|
AttachResponse,
|
|
4
4
|
AssociateRepositoryCubeRequest,
|
|
5
5
|
CreateCubeRequest,
|
|
6
|
+
DeleteCubeRequest,
|
|
6
7
|
EnrollmentExchangeRequest,
|
|
7
8
|
ResolveRepositoryCubeRequest,
|
|
8
9
|
} from '../protocol/contract.js';
|
|
10
|
+
import type { EnrichedStreamEntry } from '../protocol/types.js';
|
|
9
11
|
|
|
10
12
|
export * from './adapter.js';
|
|
11
13
|
|
|
@@ -67,6 +69,138 @@ export const DRONE_ADDRESS_CONFORMANCE: readonly ConformanceVector<string, strin
|
|
|
67
69
|
},
|
|
68
70
|
];
|
|
69
71
|
|
|
72
|
+
const APPEND_LOG_ENTRY: EnrichedStreamEntry = {
|
|
73
|
+
id: '00000000-0000-4000-8000-000000000201',
|
|
74
|
+
cube_id: '00000000-0000-4000-8000-000000000202',
|
|
75
|
+
drone_id: '00000000-0000-4000-8000-000000000203',
|
|
76
|
+
message: 'REVIEW-READY: example',
|
|
77
|
+
visibility: 'direct',
|
|
78
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
79
|
+
drone_label: 'one-of-one-builder',
|
|
80
|
+
role_name: 'Builder',
|
|
81
|
+
recipient_drone_ids: ['00000000-0000-4000-8000-000000000204'],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const APPEND_LOG_ROUTING = {
|
|
85
|
+
class: 'review',
|
|
86
|
+
recipients: ['one-of-one-reviewer'],
|
|
87
|
+
fellOpen: false,
|
|
88
|
+
message: 'Routing applied.',
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
91
|
+
export interface AppendLogResultConformanceVector {
|
|
92
|
+
name: string;
|
|
93
|
+
response: unknown;
|
|
94
|
+
accepts: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Runtime response vectors keep the append-log decoder aligned with its public type. */
|
|
98
|
+
export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceVector[] = [
|
|
99
|
+
{
|
|
100
|
+
name: 'accepts a response without optional routing metadata',
|
|
101
|
+
response: { entry: APPEND_LOG_ENTRY },
|
|
102
|
+
accepts: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'accepts exact routing metadata and unreachable recipients',
|
|
106
|
+
response: {
|
|
107
|
+
entry: APPEND_LOG_ENTRY,
|
|
108
|
+
routing: APPEND_LOG_ROUTING,
|
|
109
|
+
unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer' }],
|
|
110
|
+
},
|
|
111
|
+
accepts: true,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'accepts a null routing echo and an empty unreachable-recipient list',
|
|
115
|
+
response: { entry: APPEND_LOG_ENTRY, routing: null, unreachableRecipients: [] },
|
|
116
|
+
accepts: true,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'rejects unknown top-level response fields',
|
|
120
|
+
response: { entry: APPEND_LOG_ENTRY, routing: APPEND_LOG_ROUTING, extra: true },
|
|
121
|
+
accepts: false,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'rejects a non-object routing echo',
|
|
125
|
+
response: { entry: APPEND_LOG_ENTRY, routing: 'review' },
|
|
126
|
+
accepts: false,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'rejects missing routing fields',
|
|
130
|
+
response: { entry: APPEND_LOG_ENTRY, routing: { class: 'review' } },
|
|
131
|
+
accepts: false,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'rejects unknown routing fields',
|
|
135
|
+
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, extra: true } },
|
|
136
|
+
accepts: false,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'rejects an invalid routing class',
|
|
140
|
+
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
|
|
141
|
+
accepts: false,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'rejects an invalid routing recipient collection',
|
|
145
|
+
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
|
|
146
|
+
accepts: false,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'rejects invalid routing recipient members',
|
|
150
|
+
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
|
|
151
|
+
accepts: false,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'rejects an invalid routing fell-open flag',
|
|
155
|
+
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
|
|
156
|
+
accepts: false,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'rejects an invalid routing message',
|
|
160
|
+
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
|
|
161
|
+
accepts: false,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'rejects a non-array unreachable-recipient list',
|
|
165
|
+
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: {} },
|
|
166
|
+
accepts: false,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'rejects a non-object unreachable recipient',
|
|
170
|
+
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: ['missing-reviewer'] },
|
|
171
|
+
accepts: false,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'rejects missing unreachable-recipient fields',
|
|
175
|
+
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: [{ id: 'missing-reviewer' }] },
|
|
176
|
+
accepts: false,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'rejects unknown unreachable-recipient fields',
|
|
180
|
+
response: {
|
|
181
|
+
entry: APPEND_LOG_ENTRY,
|
|
182
|
+
unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer', extra: true }],
|
|
183
|
+
},
|
|
184
|
+
accepts: false,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'rejects an invalid unreachable-recipient id',
|
|
188
|
+
response: {
|
|
189
|
+
entry: APPEND_LOG_ENTRY,
|
|
190
|
+
unreachableRecipients: [{ id: 7, label: 'Missing Reviewer' }],
|
|
191
|
+
},
|
|
192
|
+
accepts: false,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'rejects an invalid unreachable-recipient label',
|
|
196
|
+
response: {
|
|
197
|
+
entry: APPEND_LOG_ENTRY,
|
|
198
|
+
unreachableRecipients: [{ id: 'missing-reviewer', label: null }],
|
|
199
|
+
},
|
|
200
|
+
accepts: false,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
70
204
|
export const ROLE_SECTION_ROUND_TRIP_CONFORMANCE: readonly string[] = [
|
|
71
205
|
'',
|
|
72
206
|
'Preamble only.',
|
|
@@ -191,6 +325,23 @@ const CREATE_CUBE_INITIAL: CreateCubeRequest = {
|
|
|
191
325
|
template: 'default',
|
|
192
326
|
};
|
|
193
327
|
|
|
328
|
+
export interface CubeTemplateAcceptanceConformanceVector {
|
|
329
|
+
name: string;
|
|
330
|
+
template: unknown;
|
|
331
|
+
accepts: boolean;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Protocol v6's complete closed acceptance set for cube-creation templates. */
|
|
335
|
+
export const CUBE_TEMPLATE_ACCEPTANCE_CONFORMANCE:
|
|
336
|
+
readonly CubeTemplateAcceptanceConformanceVector[] = [
|
|
337
|
+
{ name: 'accepts the legacy default template', template: 'default', accepts: true },
|
|
338
|
+
{ name: 'accepts the software-development template', template: 'software-dev', accepts: true },
|
|
339
|
+
{ name: 'accepts the starter template', template: 'starter', accepts: true },
|
|
340
|
+
{ name: 'accepts the local-model template', template: 'local-model', accepts: true },
|
|
341
|
+
{ name: 'rejects an unknown template name', template: 'custom', accepts: false },
|
|
342
|
+
{ name: 'rejects a non-string template', template: null, accepts: false },
|
|
343
|
+
];
|
|
344
|
+
|
|
194
345
|
/** Stateful vectors: name, repository identity, and template bind the retry key. */
|
|
195
346
|
export const CREATE_CUBE_RETRY_CONFORMANCE: readonly CreateCubeRetryConformanceVector[] = [
|
|
196
347
|
{
|
|
@@ -293,6 +444,58 @@ readonly CreateCubeAssociationConformanceVector[] = [
|
|
|
293
444
|
},
|
|
294
445
|
];
|
|
295
446
|
|
|
447
|
+
export interface DeleteCubeConformanceVector {
|
|
448
|
+
name: string;
|
|
449
|
+
request: DeleteCubeRequest;
|
|
450
|
+
expected: {
|
|
451
|
+
status: 200 | 403 | 404 | 410;
|
|
452
|
+
error?: 'ACCESS_DENIED' | 'NOT_FOUND' | 'CUBE_DELETED';
|
|
453
|
+
response?: { deleted: true };
|
|
454
|
+
terminal_sse?: { event: 'error'; error: 'CUBE_DELETED'; closes_after_event: true };
|
|
455
|
+
durable_after_restart?: true;
|
|
456
|
+
mutation: 'cascade' | 'none';
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Cube deletion is manage-gated, cascading, terminal, and durable across authority restart. */
|
|
461
|
+
export const DELETE_CUBE_CONFORMANCE: readonly DeleteCubeConformanceVector[] = [
|
|
462
|
+
{
|
|
463
|
+
name: 'non-member managing parent deletes the cube atomically',
|
|
464
|
+
request: {},
|
|
465
|
+
expected: { status: 200, response: { deleted: true }, mutation: 'cascade' },
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'known read and write grants and drone sessions cannot delete',
|
|
469
|
+
request: {},
|
|
470
|
+
expected: { status: 403, error: 'ACCESS_DENIED', mutation: 'none' },
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'never-authorized and unknown callers cannot enumerate the cube',
|
|
474
|
+
request: {},
|
|
475
|
+
expected: { status: 404, error: 'NOT_FOUND', mutation: 'none' },
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: 'connected clients receive one terminal error frame before close',
|
|
479
|
+
request: {},
|
|
480
|
+
expected: {
|
|
481
|
+
status: 410,
|
|
482
|
+
error: 'CUBE_DELETED',
|
|
483
|
+
terminal_sse: { event: 'error', error: 'CUBE_DELETED', closes_after_event: true },
|
|
484
|
+
mutation: 'none',
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'former authorized credentials retain the typed terminal state after restart',
|
|
489
|
+
request: {},
|
|
490
|
+
expected: {
|
|
491
|
+
status: 410,
|
|
492
|
+
error: 'CUBE_DELETED',
|
|
493
|
+
durable_after_restart: true,
|
|
494
|
+
mutation: 'none',
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
];
|
|
498
|
+
|
|
296
499
|
export interface ResolveRepositoryCubeConformanceVector {
|
|
297
500
|
name: string;
|
|
298
501
|
request: ResolveRepositoryCubeRequest;
|
package/src/protocol/contract.ts
CHANGED
|
@@ -13,12 +13,13 @@ import type {
|
|
|
13
13
|
} from './types.js';
|
|
14
14
|
|
|
15
15
|
export const SHARED_PACKAGE_NAME = 'borgmcp-shared' as const;
|
|
16
|
-
export const SHARED_PACKAGE_VERSION = '0.
|
|
16
|
+
export const SHARED_PACKAGE_VERSION = '0.7.1' as const;
|
|
17
17
|
|
|
18
18
|
export const HEALTH_PATH = '/healthz' as const;
|
|
19
19
|
export const PROTOCOL_INFO_PATH = '/api/protocol' as const;
|
|
20
20
|
export const ENROLLMENT_EXCHANGE_PATH = '/api/enrollment/exchange' as const;
|
|
21
21
|
export const CUBES_PATH = '/api/cubes' as const;
|
|
22
|
+
export const CUBE_PATH = '/api/cubes/:cubeId' as const;
|
|
22
23
|
export const REPOSITORY_CUBE_RESOLVE_PATH = '/api/repository-cubes/resolve' as const;
|
|
23
24
|
export const REPOSITORY_CUBE_ASSOCIATION_PATH = '/api/repository-cubes/association' as const;
|
|
24
25
|
export const ATTACH_PATH = '/api/client/attach' as const;
|
|
@@ -29,6 +30,13 @@ export const PROTOCOL_HTTP_CONTRACT = {
|
|
|
29
30
|
protocol: { method: 'GET', path: PROTOCOL_INFO_PATH, authenticated: false, success_status: 200 },
|
|
30
31
|
enrollment: { method: 'POST', path: ENROLLMENT_EXCHANGE_PATH, authenticated: 'invitation', success_status: 201 },
|
|
31
32
|
cubes: { method: 'POST', path: CUBES_PATH, authenticated: true, success_status: 201 },
|
|
33
|
+
cube_delete: {
|
|
34
|
+
method: 'DELETE',
|
|
35
|
+
path: CUBE_PATH,
|
|
36
|
+
authenticated: true,
|
|
37
|
+
success_status: 200,
|
|
38
|
+
mutation: true,
|
|
39
|
+
},
|
|
32
40
|
repository_cube_resolve: {
|
|
33
41
|
method: 'POST',
|
|
34
42
|
path: REPOSITORY_CUBE_RESOLVE_PATH,
|
|
@@ -68,6 +76,7 @@ export const PROTOCOL_HTTP_CONTRACT = {
|
|
|
68
76
|
session_revoked_status: 401,
|
|
69
77
|
session_rejected_status: 401,
|
|
70
78
|
cursor_expired_status: 410,
|
|
79
|
+
cube_deleted_status: 410,
|
|
71
80
|
drone_evicted_status: 410,
|
|
72
81
|
content_too_large_status: 413,
|
|
73
82
|
unsupported_protocol_status: 426,
|
|
@@ -137,7 +146,7 @@ export type EnrollmentExchangeResponse =
|
|
|
137
146
|
| ClientEnrollmentExchangeResponse
|
|
138
147
|
| OwnerEnrollmentExchangeResponse;
|
|
139
148
|
|
|
140
|
-
export const CUBE_TEMPLATES = ['default', 'software-dev', 'starter'] as const;
|
|
149
|
+
export const CUBE_TEMPLATES = ['default', 'software-dev', 'starter', 'local-model'] as const;
|
|
141
150
|
export type CubeTemplate = (typeof CUBE_TEMPLATES)[number];
|
|
142
151
|
|
|
143
152
|
export type CreateCubeRepository =
|
|
@@ -164,6 +173,13 @@ export interface CreateCubeResponse {
|
|
|
164
173
|
access: 'manage';
|
|
165
174
|
}
|
|
166
175
|
|
|
176
|
+
export type DeleteCubeRequest = Record<string, never>;
|
|
177
|
+
|
|
178
|
+
export interface DeleteCubeResponse {
|
|
179
|
+
cube_id: string;
|
|
180
|
+
deleted: true;
|
|
181
|
+
}
|
|
182
|
+
|
|
167
183
|
export interface ResolveRepositoryCubeRequest {
|
|
168
184
|
working_repo_name: string;
|
|
169
185
|
repository: CreateCubeRepository;
|
|
@@ -327,7 +343,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
|
|
|
327
343
|
exactKeys(input, ['protocol_version'], ['protocol_version']);
|
|
328
344
|
if (input.protocol_version !== PROTOCOL_VERSION) {
|
|
329
345
|
throw new ProtocolContractError(
|
|
330
|
-
'
|
|
346
|
+
'This client requires protocol v7. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
|
|
331
347
|
ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
|
|
332
348
|
['protocol_version'],
|
|
333
349
|
);
|
|
@@ -596,6 +612,30 @@ export function decodeCreateCubeResponseEnvelope(value: unknown): ProtocolEnvelo
|
|
|
596
612
|
return decodeProtocolEnvelope(value, decodeCreateCubeResponse);
|
|
597
613
|
}
|
|
598
614
|
|
|
615
|
+
export function decodeDeleteCubeRequest(value: unknown): DeleteCubeRequest {
|
|
616
|
+
const input = record(value);
|
|
617
|
+
exactKeys(input, [], []);
|
|
618
|
+
return {};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function decodeDeleteCubeRequestEnvelope(value: unknown): ProtocolEnvelope<DeleteCubeRequest> {
|
|
622
|
+
return decodeProtocolEnvelope(value, decodeDeleteCubeRequest);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function decodeDeleteCubeResponse(value: unknown): DeleteCubeResponse {
|
|
626
|
+
const input = record(value);
|
|
627
|
+
exactKeys(input, ['cube_id', 'deleted'], ['cube_id', 'deleted']);
|
|
628
|
+
if (input.deleted !== true) fail('Cube deletion result must be terminal.', ['deleted']);
|
|
629
|
+
return {
|
|
630
|
+
cube_id: decodeUuid(input.cube_id, ['cube_id']),
|
|
631
|
+
deleted: true,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function decodeDeleteCubeResponseEnvelope(value: unknown): ProtocolEnvelope<DeleteCubeResponse> {
|
|
636
|
+
return decodeProtocolEnvelope(value, decodeDeleteCubeResponse);
|
|
637
|
+
}
|
|
638
|
+
|
|
599
639
|
export function decodeResolveRepositoryCubeRequest(value: unknown): ResolveRepositoryCubeRequest {
|
|
600
640
|
const input = record(value);
|
|
601
641
|
exactKeys(input, ['working_repo_name', 'repository'], ['working_repo_name', 'repository']);
|