borgmcp-shared 0.11.0 → 0.12.2
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 +7 -1
- package/RELEASES.md +14 -0
- package/dist/conformance/adapter.d.ts +4 -0
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +109 -10
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +13 -0
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +84 -14
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +2 -1
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +9 -4
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +8 -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/types.d.ts +2 -0
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/docs/release-records.json +117 -0
- package/docs/releases/0.12.0.md +7 -0
- package/docs/releases/0.12.1.md +7 -0
- package/docs/releases/0.12.2.md +7 -0
- package/docs/releasing.md +62 -75
- package/package.json +2 -1
- package/src/conformance/adapter.ts +137 -9
- package/src/conformance/index.ts +102 -15
- package/src/protocol/contract.ts +11 -5
- package/src/protocol/coordination.ts +8 -2
- package/src/protocol/errors.ts +1 -0
- package/src/protocol/types.ts +2 -0
- package/src/protocol/version.ts +2 -2
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type DroneRuntimeMetadata,
|
|
38
38
|
} from '../protocol/index.js';
|
|
39
39
|
import {
|
|
40
|
+
APPEND_LOG_IDEMPOTENCY_CONFORMANCE,
|
|
40
41
|
CREATE_CUBE_ASSOCIATION_CONFORMANCE,
|
|
41
42
|
CREATE_CUBE_RETRY_CONFORMANCE,
|
|
42
43
|
INVITATION_ARTIFACT_CONFORMANCE,
|
|
@@ -208,6 +209,11 @@ export interface ConformanceAdmin {
|
|
|
208
209
|
readonly isMandatory?: boolean;
|
|
209
210
|
}): Promise<ConformanceRole>;
|
|
210
211
|
referenceRoleFromTaxonomy(cube: ConformanceCube, role: ConformanceRole): Promise<void>;
|
|
212
|
+
configureMessageClassRouting(
|
|
213
|
+
cube: ConformanceCube,
|
|
214
|
+
className: string,
|
|
215
|
+
recipientDroneIds: readonly string[],
|
|
216
|
+
): Promise<void>;
|
|
211
217
|
createDrone(
|
|
212
218
|
principal: ConformancePrincipal,
|
|
213
219
|
cube: ConformanceCube,
|
|
@@ -361,6 +367,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
361
367
|
{ id: 'security.adapter-boundary-injection', area: 'security' },
|
|
362
368
|
{ id: 'security.oversize-request', area: 'security' },
|
|
363
369
|
{ id: 'security.cross-cube-isolation', area: 'security' },
|
|
370
|
+
{ id: 'log.append-idempotency', area: 'log' },
|
|
364
371
|
{ id: 'log.read-cursor-tuple', area: 'cursor' },
|
|
365
372
|
{ id: 'sse.replay-live-transition', area: 'sse' },
|
|
366
373
|
{ id: 'cursor.explicit-expiry', area: 'cursor' },
|
|
@@ -1323,7 +1330,10 @@ export async function runAdapterConformance(
|
|
|
1323
1330
|
await record('security.adapter-boundary-injection', async () => {
|
|
1324
1331
|
const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
|
|
1325
1332
|
const injectedBody = JSON.stringify(
|
|
1326
|
-
createProtocolEnvelope('inject-b1', {
|
|
1333
|
+
createProtocolEnvelope('inject-b1', {
|
|
1334
|
+
post_id: '00000000-0000-4000-8000-000000000301',
|
|
1335
|
+
message: injectedMessage,
|
|
1336
|
+
}),
|
|
1327
1337
|
);
|
|
1328
1338
|
invariant(
|
|
1329
1339
|
utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
|
|
@@ -1342,7 +1352,10 @@ export async function runAdapterConformance(
|
|
|
1342
1352
|
const sentinel = await environment.operations.append(
|
|
1343
1353
|
credentialB,
|
|
1344
1354
|
cubeB,
|
|
1345
|
-
createProtocolEnvelope('inject-b2', {
|
|
1355
|
+
createProtocolEnvelope('inject-b2', {
|
|
1356
|
+
post_id: '00000000-0000-4000-8000-000000000302',
|
|
1357
|
+
message: 'post-injection-sentinel',
|
|
1358
|
+
}),
|
|
1346
1359
|
);
|
|
1347
1360
|
expectStatus(sentinel, 201, 'Post-injection sentinel append');
|
|
1348
1361
|
const read = await environment.operations.read(
|
|
@@ -1389,7 +1402,10 @@ export async function runAdapterConformance(
|
|
|
1389
1402
|
|
|
1390
1403
|
await record('security.oversize-request', async () => {
|
|
1391
1404
|
const baseBody = JSON.stringify(
|
|
1392
|
-
createProtocolEnvelope('oversize-a1', {
|
|
1405
|
+
createProtocolEnvelope('oversize-a1', {
|
|
1406
|
+
post_id: '00000000-0000-4000-8000-000000000303',
|
|
1407
|
+
message: 'must-not-persist',
|
|
1408
|
+
}),
|
|
1393
1409
|
);
|
|
1394
1410
|
const oversizedBody = baseBody + ' '.repeat(
|
|
1395
1411
|
Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
|
|
@@ -1417,7 +1433,10 @@ export async function runAdapterConformance(
|
|
|
1417
1433
|
const secretAppend = await environment.operations.append(
|
|
1418
1434
|
credentialB,
|
|
1419
1435
|
cubeB,
|
|
1420
|
-
createProtocolEnvelope('append-b1', {
|
|
1436
|
+
createProtocolEnvelope('append-b1', {
|
|
1437
|
+
post_id: '00000000-0000-4000-8000-000000000304',
|
|
1438
|
+
message: 'principal-b-secret',
|
|
1439
|
+
}),
|
|
1421
1440
|
);
|
|
1422
1441
|
expectStatus(secretAppend, 201, 'Principal B append');
|
|
1423
1442
|
const denied = await environment.operations.read(
|
|
@@ -1429,6 +1448,99 @@ export async function runAdapterConformance(
|
|
|
1429
1448
|
return { status: 404, code: ErrorCode.NOT_FOUND };
|
|
1430
1449
|
});
|
|
1431
1450
|
|
|
1451
|
+
await record('log.append-idempotency', async () => {
|
|
1452
|
+
const principal = await environment.admin.createPrincipal('append-idempotency');
|
|
1453
|
+
const otherPrincipal = await environment.admin.createPrincipal('append-idempotency-other');
|
|
1454
|
+
const cube = await environment.admin.createCube('append-idempotency');
|
|
1455
|
+
await environment.admin.grantCube(principal, cube);
|
|
1456
|
+
await environment.admin.grantCube(otherPrincipal, cube);
|
|
1457
|
+
const credential = await environment.admin.issueDroneSession(principal);
|
|
1458
|
+
const otherCredential = await environment.admin.issueDroneSession(otherPrincipal);
|
|
1459
|
+
const role = await environment.admin.createRole(cube, {
|
|
1460
|
+
roleClass: 'worker',
|
|
1461
|
+
isHumanSeat: false,
|
|
1462
|
+
});
|
|
1463
|
+
const firstRecipient = await environment.admin.createDrone(principal, cube, role);
|
|
1464
|
+
const secondRecipient = await environment.admin.createDrone(principal, cube, role);
|
|
1465
|
+
await environment.admin.configureMessageClassRouting(cube, 'conformance-review', [firstRecipient.id]);
|
|
1466
|
+
await environment.admin.configureMessageClassRouting(cube, 'conformance-security', [firstRecipient.id]);
|
|
1467
|
+
const postId = '00000000-0000-4000-8000-000000000313';
|
|
1468
|
+
const initialPayload = {
|
|
1469
|
+
post_id: postId,
|
|
1470
|
+
message: 'once',
|
|
1471
|
+
visibility: 'direct' as const,
|
|
1472
|
+
recipientDroneIds: [firstRecipient.id],
|
|
1473
|
+
class: 'review',
|
|
1474
|
+
to: ['Coordinator'],
|
|
1475
|
+
};
|
|
1476
|
+
const first = await environment.operations.append(
|
|
1477
|
+
credential,
|
|
1478
|
+
cube,
|
|
1479
|
+
createProtocolEnvelope('append-idempotency-first', initialPayload),
|
|
1480
|
+
);
|
|
1481
|
+
expectStatus(first, 201, 'Initial idempotent append');
|
|
1482
|
+
const firstResult = decodeAppendLogResultEnvelope(first.body).payload;
|
|
1483
|
+
invariant(!firstResult.deduplicated, 'Initial append was reported as deduplicated.');
|
|
1484
|
+
for (const [index, vector] of APPEND_LOG_IDEMPOTENCY_CONFORMANCE.entries()) {
|
|
1485
|
+
const classRouting = vector.mutation === 'resolved_class_routing';
|
|
1486
|
+
const vectorPostId = classRouting
|
|
1487
|
+
? '00000000-0000-4000-8000-000000000314'
|
|
1488
|
+
: postId;
|
|
1489
|
+
if (classRouting) {
|
|
1490
|
+
const baseline = await environment.operations.append(
|
|
1491
|
+
credential,
|
|
1492
|
+
cube,
|
|
1493
|
+
createProtocolEnvelope('append-idempotency-class-baseline', {
|
|
1494
|
+
post_id: vectorPostId,
|
|
1495
|
+
message: 'once',
|
|
1496
|
+
class: 'conformance-review',
|
|
1497
|
+
}),
|
|
1498
|
+
);
|
|
1499
|
+
expectStatus(baseline, 201, 'Resolved class-routing baseline');
|
|
1500
|
+
}
|
|
1501
|
+
const payload = classRouting ? {
|
|
1502
|
+
post_id: vectorPostId,
|
|
1503
|
+
message: 'once',
|
|
1504
|
+
class: 'conformance-security',
|
|
1505
|
+
} : {
|
|
1506
|
+
...initialPayload,
|
|
1507
|
+
...(vector.mutation === 'message' ? { message: 'changed' } : {}),
|
|
1508
|
+
...(vector.mutation === 'visibility' ? { visibility: 'broadcast' as const } : {}),
|
|
1509
|
+
...(vector.mutation === 'recipient_set' ? { recipientDroneIds: [secondRecipient.id] } : {}),
|
|
1510
|
+
...(vector.mutation === 'ignored_request_shape' ? { class: 'security', to: ['Security Auditor'] } : {}),
|
|
1511
|
+
};
|
|
1512
|
+
const response = await environment.operations.append(
|
|
1513
|
+
vector.actor === 'same' ? credential : otherCredential,
|
|
1514
|
+
cube,
|
|
1515
|
+
createProtocolEnvelope(`append-idempotency-${index}`, payload),
|
|
1516
|
+
);
|
|
1517
|
+
if (vector.expected === 'POST_ID_CONFLICT') {
|
|
1518
|
+
expectError(response, 409, ErrorCode.POST_ID_CONFLICT, vector.name);
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
expectStatus(response, 201, vector.name);
|
|
1522
|
+
const result = decodeAppendLogResultEnvelope(response.body).payload;
|
|
1523
|
+
invariant(result.deduplicated === (vector.expected === 'deduplicated'), `${vector.name} returned the wrong deduplicated flag.`);
|
|
1524
|
+
invariant(
|
|
1525
|
+
vector.expected === 'deduplicated'
|
|
1526
|
+
? result.entry.id === firstResult.entry.id
|
|
1527
|
+
: result.entry.id !== firstResult.entry.id,
|
|
1528
|
+
`${vector.name} returned the wrong entry identity.`,
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const read = await environment.operations.read(
|
|
1532
|
+
credential,
|
|
1533
|
+
cube,
|
|
1534
|
+
createProtocolEnvelope('append-idempotency-read', { cursor: null, limit: 10 }),
|
|
1535
|
+
);
|
|
1536
|
+
expectStatus(read, 200, 'Idempotent append read');
|
|
1537
|
+
invariant(
|
|
1538
|
+
decodeReadLogResultEnvelope(read.body).payload.entries.length === 3,
|
|
1539
|
+
'Append collision controls persisted the wrong number of entries.',
|
|
1540
|
+
);
|
|
1541
|
+
return { persisted_entries: 3, stable_entry: true, deduplicated: true, conflicts: 4, cross_author_created: true };
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1432
1544
|
const entries: Array<{ id: string; created_at: string; message: string }> = [];
|
|
1433
1545
|
let readCursor: LogCursor | null = null;
|
|
1434
1546
|
await record('log.read-cursor-tuple', async () => {
|
|
@@ -1436,7 +1548,10 @@ export async function runAdapterConformance(
|
|
|
1436
1548
|
const response = await environment.operations.append(
|
|
1437
1549
|
credentialA,
|
|
1438
1550
|
cubeA,
|
|
1439
|
-
createProtocolEnvelope(`append-a${index + 1}`, {
|
|
1551
|
+
createProtocolEnvelope(`append-a${index + 1}`, {
|
|
1552
|
+
post_id: `00000000-0000-4000-8000-${String(305 + index).padStart(12, '0')}`,
|
|
1553
|
+
message,
|
|
1554
|
+
}),
|
|
1440
1555
|
);
|
|
1441
1556
|
expectStatus(response, 201, `Append ${message}`);
|
|
1442
1557
|
const entry = decodeAppendLogResultEnvelope(response.body).payload.entry;
|
|
@@ -1470,7 +1585,10 @@ export async function runAdapterConformance(
|
|
|
1470
1585
|
const appendDelta = environment.operations.append(
|
|
1471
1586
|
credentialA,
|
|
1472
1587
|
cubeA,
|
|
1473
|
-
createProtocolEnvelope('append-a4', {
|
|
1588
|
+
createProtocolEnvelope('append-a4', {
|
|
1589
|
+
post_id: '00000000-0000-4000-8000-000000000308',
|
|
1590
|
+
message: 'delta',
|
|
1591
|
+
}),
|
|
1474
1592
|
);
|
|
1475
1593
|
expectStatus(await appendDelta, 201, 'Transition append');
|
|
1476
1594
|
} finally {
|
|
@@ -1494,7 +1612,10 @@ export async function runAdapterConformance(
|
|
|
1494
1612
|
const epsilonResponse = await environment.operations.append(
|
|
1495
1613
|
credentialA,
|
|
1496
1614
|
cubeA,
|
|
1497
|
-
createProtocolEnvelope('append-a5', {
|
|
1615
|
+
createProtocolEnvelope('append-a5', {
|
|
1616
|
+
post_id: '00000000-0000-4000-8000-000000000309',
|
|
1617
|
+
message: 'epsilon',
|
|
1618
|
+
}),
|
|
1498
1619
|
);
|
|
1499
1620
|
expectStatus(epsilonResponse, 201, 'Live append');
|
|
1500
1621
|
const epsilon = logEvent(await within(noDuplicate, 'Live epsilon event', streamDeadlineMs), 'Live append');
|
|
@@ -2014,6 +2135,7 @@ export async function runAdapterConformance(
|
|
|
2014
2135
|
cubeA,
|
|
2015
2136
|
createProtocolEnvelope('evict-direct-target', {
|
|
2016
2137
|
message: 'must-not-fan-out',
|
|
2138
|
+
post_id: '00000000-0000-4000-8000-000000000310',
|
|
2017
2139
|
visibility: 'direct',
|
|
2018
2140
|
recipientDroneIds: [evictedDrone.id],
|
|
2019
2141
|
}),
|
|
@@ -2203,7 +2325,10 @@ export async function runAdapterConformance(
|
|
|
2203
2325
|
const attributed = await environment.operations.append(
|
|
2204
2326
|
evictedSession,
|
|
2205
2327
|
roleContractCube,
|
|
2206
|
-
createProtocolEnvelope('role-delete-attribution', {
|
|
2328
|
+
createProtocolEnvelope('role-delete-attribution', {
|
|
2329
|
+
post_id: '00000000-0000-4000-8000-000000000311',
|
|
2330
|
+
message: 'attribution survives',
|
|
2331
|
+
}),
|
|
2207
2332
|
);
|
|
2208
2333
|
expectStatus(attributed, 201, 'Pre-delete attributed log');
|
|
2209
2334
|
const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
|
|
@@ -2867,7 +2992,10 @@ export async function runAdapterConformance(
|
|
|
2867
2992
|
const append = await environment.operations.append(
|
|
2868
2993
|
creator.credential,
|
|
2869
2994
|
cube,
|
|
2870
|
-
createProtocolEnvelope('delete-log', {
|
|
2995
|
+
createProtocolEnvelope('delete-log', {
|
|
2996
|
+
post_id: '00000000-0000-4000-8000-000000000312',
|
|
2997
|
+
message: 'deleted with cube',
|
|
2998
|
+
}),
|
|
2871
2999
|
);
|
|
2872
3000
|
expectStatus(append, 201, 'Deletion fixture log');
|
|
2873
3001
|
const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
|
package/src/conformance/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
encodeInvitationArtifact,
|
|
12
12
|
type InvitationArtifact,
|
|
13
13
|
} from '../protocol/contract.js';
|
|
14
|
-
import type { EnrichedStreamEntry } from '../protocol/types.js';
|
|
14
|
+
import type { AppendLogRequest, EnrichedStreamEntry } from '../protocol/types.js';
|
|
15
15
|
import { ROLE_IN_USE_DELETE_MESSAGE } from '../protocol/coordination.js';
|
|
16
16
|
|
|
17
17
|
export * from './adapter.js';
|
|
@@ -99,17 +99,54 @@ export interface AppendLogResultConformanceVector {
|
|
|
99
99
|
accepts: boolean;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
export interface AppendLogRequestConformanceVector {
|
|
103
|
+
name: string;
|
|
104
|
+
request: unknown;
|
|
105
|
+
accepts: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface AppendLogIdempotencyConformanceVector {
|
|
109
|
+
name: string;
|
|
110
|
+
actor: 'same' | 'different';
|
|
111
|
+
mutation: 'none' | 'message' | 'visibility' | 'recipient_set' | 'ignored_request_shape' | 'resolved_class_routing';
|
|
112
|
+
expected: 'deduplicated' | 'POST_ID_CONFLICT' | 'created';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Author-scoped post identity and exact resolved-routing retry outcomes. */
|
|
116
|
+
export const APPEND_LOG_IDEMPOTENCY_CONFORMANCE: readonly AppendLogIdempotencyConformanceVector[] = [
|
|
117
|
+
{ name: 'deduplicates an exact same-author retry', actor: 'same', mutation: 'none', expected: 'deduplicated' },
|
|
118
|
+
{ name: 'rejects a same-author message collision', actor: 'same', mutation: 'message', expected: 'POST_ID_CONFLICT' },
|
|
119
|
+
{ name: 'rejects a same-author visibility collision', actor: 'same', mutation: 'visibility', expected: 'POST_ID_CONFLICT' },
|
|
120
|
+
{ name: 'rejects a same-author recipient-set collision', actor: 'same', mutation: 'recipient_set', expected: 'POST_ID_CONFLICT' },
|
|
121
|
+
{ name: 'deduplicates ignored class selectors with identical resolved delivery', actor: 'same', mutation: 'ignored_request_shape', expected: 'deduplicated' },
|
|
122
|
+
{ name: 'rejects a same-author resolved class-routing collision', actor: 'same', mutation: 'resolved_class_routing', expected: 'POST_ID_CONFLICT' },
|
|
123
|
+
{ name: 'creates an independent cross-author post', actor: 'different', mutation: 'none', expected: 'created' },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const APPEND_LOG_REQUEST = {
|
|
127
|
+
post_id: '00000000-0000-4000-8000-000000000205',
|
|
128
|
+
message: 'REVIEW-READY: example',
|
|
129
|
+
} satisfies AppendLogRequest;
|
|
130
|
+
|
|
131
|
+
export const APPEND_LOG_REQUEST_CONFORMANCE: readonly AppendLogRequestConformanceVector[] = [
|
|
132
|
+
{ name: 'accepts a canonical post UUID', request: APPEND_LOG_REQUEST, accepts: true },
|
|
133
|
+
{ name: 'rejects an omitted post UUID', request: { message: APPEND_LOG_REQUEST.message }, accepts: false },
|
|
134
|
+
{ name: 'rejects an invalid post UUID', request: { ...APPEND_LOG_REQUEST, post_id: 'not-a-uuid' }, accepts: false },
|
|
135
|
+
{ name: 'rejects unknown request fields', request: { ...APPEND_LOG_REQUEST, extra: true }, accepts: false },
|
|
136
|
+
];
|
|
137
|
+
|
|
102
138
|
/** Runtime response vectors keep the append-log decoder aligned with its public type. */
|
|
103
139
|
export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceVector[] = [
|
|
104
140
|
{
|
|
105
141
|
name: 'accepts a response without optional routing metadata',
|
|
106
|
-
response: { entry: APPEND_LOG_ENTRY },
|
|
142
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false },
|
|
107
143
|
accepts: true,
|
|
108
144
|
},
|
|
109
145
|
{
|
|
110
146
|
name: 'accepts exact routing metadata and unreachable recipients',
|
|
111
147
|
response: {
|
|
112
148
|
entry: APPEND_LOG_ENTRY,
|
|
149
|
+
deduplicated: false,
|
|
113
150
|
routing: APPEND_LOG_ROUTING,
|
|
114
151
|
unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer' }],
|
|
115
152
|
},
|
|
@@ -117,73 +154,84 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
|
|
|
117
154
|
},
|
|
118
155
|
{
|
|
119
156
|
name: 'accepts a null routing echo and an empty unreachable-recipient list',
|
|
120
|
-
response: { entry: APPEND_LOG_ENTRY, routing: null, unreachableRecipients: [] },
|
|
157
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: true, routing: null, unreachableRecipients: [] },
|
|
121
158
|
accepts: true,
|
|
122
159
|
},
|
|
160
|
+
{
|
|
161
|
+
name: 'rejects an omitted deduplication result',
|
|
162
|
+
response: { entry: APPEND_LOG_ENTRY },
|
|
163
|
+
accepts: false,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'rejects a non-boolean deduplication result',
|
|
167
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: 'false' },
|
|
168
|
+
accepts: false,
|
|
169
|
+
},
|
|
123
170
|
{
|
|
124
171
|
name: 'rejects unknown top-level response fields',
|
|
125
|
-
response: { entry: APPEND_LOG_ENTRY, routing: APPEND_LOG_ROUTING, extra: true },
|
|
172
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: APPEND_LOG_ROUTING, extra: true },
|
|
126
173
|
accepts: false,
|
|
127
174
|
},
|
|
128
175
|
{
|
|
129
176
|
name: 'rejects a non-object routing echo',
|
|
130
|
-
response: { entry: APPEND_LOG_ENTRY, routing: 'review' },
|
|
177
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: 'review' },
|
|
131
178
|
accepts: false,
|
|
132
179
|
},
|
|
133
180
|
{
|
|
134
181
|
name: 'rejects missing routing fields',
|
|
135
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { class: 'review' } },
|
|
182
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { class: 'review' } },
|
|
136
183
|
accepts: false,
|
|
137
184
|
},
|
|
138
185
|
{
|
|
139
186
|
name: 'rejects unknown routing fields',
|
|
140
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, extra: true } },
|
|
187
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, extra: true } },
|
|
141
188
|
accepts: false,
|
|
142
189
|
},
|
|
143
190
|
{
|
|
144
191
|
name: 'rejects an invalid routing class',
|
|
145
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
|
|
192
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
|
|
146
193
|
accepts: false,
|
|
147
194
|
},
|
|
148
195
|
{
|
|
149
196
|
name: 'rejects an invalid routing recipient collection',
|
|
150
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
|
|
197
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
|
|
151
198
|
accepts: false,
|
|
152
199
|
},
|
|
153
200
|
{
|
|
154
201
|
name: 'rejects invalid routing recipient members',
|
|
155
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
|
|
202
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
|
|
156
203
|
accepts: false,
|
|
157
204
|
},
|
|
158
205
|
{
|
|
159
206
|
name: 'rejects an invalid routing fell-open flag',
|
|
160
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
|
|
207
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
|
|
161
208
|
accepts: false,
|
|
162
209
|
},
|
|
163
210
|
{
|
|
164
211
|
name: 'rejects an invalid routing message',
|
|
165
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
|
|
212
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
|
|
166
213
|
accepts: false,
|
|
167
214
|
},
|
|
168
215
|
{
|
|
169
216
|
name: 'rejects a non-array unreachable-recipient list',
|
|
170
|
-
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: {} },
|
|
217
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: {} },
|
|
171
218
|
accepts: false,
|
|
172
219
|
},
|
|
173
220
|
{
|
|
174
221
|
name: 'rejects a non-object unreachable recipient',
|
|
175
|
-
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: ['missing-reviewer'] },
|
|
222
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: ['missing-reviewer'] },
|
|
176
223
|
accepts: false,
|
|
177
224
|
},
|
|
178
225
|
{
|
|
179
226
|
name: 'rejects missing unreachable-recipient fields',
|
|
180
|
-
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: [{ id: 'missing-reviewer' }] },
|
|
227
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: [{ id: 'missing-reviewer' }] },
|
|
181
228
|
accepts: false,
|
|
182
229
|
},
|
|
183
230
|
{
|
|
184
231
|
name: 'rejects unknown unreachable-recipient fields',
|
|
185
232
|
response: {
|
|
186
233
|
entry: APPEND_LOG_ENTRY,
|
|
234
|
+
deduplicated: false,
|
|
187
235
|
unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer', extra: true }],
|
|
188
236
|
},
|
|
189
237
|
accepts: false,
|
|
@@ -192,6 +240,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
|
|
|
192
240
|
name: 'rejects an invalid unreachable-recipient id',
|
|
193
241
|
response: {
|
|
194
242
|
entry: APPEND_LOG_ENTRY,
|
|
243
|
+
deduplicated: false,
|
|
195
244
|
unreachableRecipients: [{ id: 7, label: 'Missing Reviewer' }],
|
|
196
245
|
},
|
|
197
246
|
accepts: false,
|
|
@@ -200,6 +249,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
|
|
|
200
249
|
name: 'rejects an invalid unreachable-recipient label',
|
|
201
250
|
response: {
|
|
202
251
|
entry: APPEND_LOG_ENTRY,
|
|
252
|
+
deduplicated: false,
|
|
203
253
|
unreachableRecipients: [{ id: 'missing-reviewer', label: null }],
|
|
204
254
|
},
|
|
205
255
|
accepts: false,
|
|
@@ -750,11 +800,48 @@ const ATTACH_RESPONSE = {
|
|
|
750
800
|
runtime_metadata_reported: false,
|
|
751
801
|
},
|
|
752
802
|
session: { id: '40000000-0000-4000-8000-000000000001' },
|
|
803
|
+
initial_log_cursor: null,
|
|
753
804
|
} satisfies AttachResponse;
|
|
754
805
|
|
|
755
806
|
/** Wire vectors for the v3 non-expiring attach-session response. */
|
|
756
807
|
export const ATTACH_SESSION_CONFORMANCE: readonly AttachSessionConformanceVector[] = [
|
|
757
808
|
{ name: 'accepts exact non-expiring session id', response: ATTACH_RESPONSE, accepts: true },
|
|
809
|
+
{
|
|
810
|
+
name: 'accepts a canonical initial log cursor',
|
|
811
|
+
response: {
|
|
812
|
+
...ATTACH_RESPONSE,
|
|
813
|
+
initial_log_cursor: {
|
|
814
|
+
id: '50000000-0000-4000-8000-000000000001',
|
|
815
|
+
created_at: '2026-07-14T10:00:00.000Z',
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
accepts: true,
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: 'rejects an omitted initial log cursor',
|
|
822
|
+
response: (({ initial_log_cursor: _, ...response }) => response)(ATTACH_RESPONSE),
|
|
823
|
+
accepts: false,
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
name: 'rejects an invalid initial log cursor UUID',
|
|
827
|
+
response: {
|
|
828
|
+
...ATTACH_RESPONSE,
|
|
829
|
+
initial_log_cursor: { id: 'not-a-uuid', created_at: '2026-07-14T10:00:00.000Z' },
|
|
830
|
+
},
|
|
831
|
+
accepts: false,
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
name: 'rejects unknown initial log cursor fields',
|
|
835
|
+
response: {
|
|
836
|
+
...ATTACH_RESPONSE,
|
|
837
|
+
initial_log_cursor: {
|
|
838
|
+
id: '50000000-0000-4000-8000-000000000001',
|
|
839
|
+
created_at: '2026-07-14T10:00:00.000Z',
|
|
840
|
+
extra: true,
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
accepts: false,
|
|
844
|
+
},
|
|
758
845
|
{
|
|
759
846
|
name: 'rejects retired expires_at field',
|
|
760
847
|
response: {
|
package/src/protocol/contract.ts
CHANGED
|
@@ -13,7 +13,7 @@ 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.12.2' as const;
|
|
17
17
|
/** Maximum UTF-8 payload for each newly recorded decision text field. */
|
|
18
18
|
export const DECISION_TEXT_MAX_BYTES = 512 as const;
|
|
19
19
|
/** Maximum UTF-8 size of role detailed-description text and any returned section slice. */
|
|
@@ -580,7 +580,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
|
|
|
580
580
|
exactKeys(input, ['protocol_version'], ['protocol_version']);
|
|
581
581
|
if (input.protocol_version !== PROTOCOL_VERSION) {
|
|
582
582
|
throw new ProtocolContractError(
|
|
583
|
-
'This client requires protocol
|
|
583
|
+
'This client requires protocol v9. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
|
|
584
584
|
ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
|
|
585
585
|
['protocol_version'],
|
|
586
586
|
);
|
|
@@ -997,10 +997,11 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
|
|
|
997
997
|
const input = record(value);
|
|
998
998
|
exactKeys(
|
|
999
999
|
input,
|
|
1000
|
-
['message', 'visibility', 'recipientDroneIds', 'class', 'to'],
|
|
1001
|
-
['message'],
|
|
1000
|
+
['post_id', 'message', 'visibility', 'recipientDroneIds', 'class', 'to'],
|
|
1001
|
+
['post_id', 'message'],
|
|
1002
1002
|
);
|
|
1003
1003
|
const output: import('./types.js').AppendLogRequest = {
|
|
1004
|
+
post_id: decodeUuid(input.post_id, ['post_id']),
|
|
1004
1005
|
message: boundedString(input.message, 1, 10_240, ['message']),
|
|
1005
1006
|
};
|
|
1006
1007
|
if (input.visibility !== undefined) {
|
|
@@ -1259,6 +1260,7 @@ export interface AttachResponse {
|
|
|
1259
1260
|
role: AttachRole;
|
|
1260
1261
|
drone: AttachDrone;
|
|
1261
1262
|
session: AttachSession;
|
|
1263
|
+
initial_log_cursor: LogCursor | null;
|
|
1262
1264
|
}
|
|
1263
1265
|
|
|
1264
1266
|
function decodeAttachCube(value: unknown, path: readonly (string | number)[]): AttachCube {
|
|
@@ -1393,12 +1395,13 @@ export function decodeAttachRequestEnvelope(
|
|
|
1393
1395
|
*/
|
|
1394
1396
|
export function decodeAttachResponse(value: unknown): AttachResponse {
|
|
1395
1397
|
const input = record(value);
|
|
1396
|
-
exactKeys(input, ['result', 'cube', 'role', 'drone', 'session'], [
|
|
1398
|
+
exactKeys(input, ['result', 'cube', 'role', 'drone', 'session', 'initial_log_cursor'], [
|
|
1397
1399
|
'result',
|
|
1398
1400
|
'cube',
|
|
1399
1401
|
'role',
|
|
1400
1402
|
'drone',
|
|
1401
1403
|
'session',
|
|
1404
|
+
'initial_log_cursor',
|
|
1402
1405
|
]);
|
|
1403
1406
|
if (input.result !== 'created' && input.result !== 'reused') {
|
|
1404
1407
|
fail('Expected result "created" or "reused".', ['result']);
|
|
@@ -1409,6 +1412,9 @@ export function decodeAttachResponse(value: unknown): AttachResponse {
|
|
|
1409
1412
|
role: decodeAttachRole(input.role, ['role']),
|
|
1410
1413
|
drone: decodeAttachDrone(input.drone, ['drone']),
|
|
1411
1414
|
session: decodeAttachSession(input.session, ['session']),
|
|
1415
|
+
initial_log_cursor: input.initial_log_cursor === null
|
|
1416
|
+
? null
|
|
1417
|
+
: decodeLogCursor(input.initial_log_cursor, ['initial_log_cursor']),
|
|
1412
1418
|
};
|
|
1413
1419
|
}
|
|
1414
1420
|
|
|
@@ -376,8 +376,14 @@ function decodeUnreachableRecipient(
|
|
|
376
376
|
|
|
377
377
|
export function decodeAppendLogResult(value: unknown): AppendLogResult {
|
|
378
378
|
const input = object(value);
|
|
379
|
-
exact(input, ['entry', 'routing', 'unreachableRecipients'], ['entry']);
|
|
380
|
-
|
|
379
|
+
exact(input, ['entry', 'deduplicated', 'routing', 'unreachableRecipients'], ['entry', 'deduplicated']);
|
|
380
|
+
if (typeof input.deduplicated !== 'boolean') {
|
|
381
|
+
throw new ProtocolContractError('Invalid append-log deduplicated flag.');
|
|
382
|
+
}
|
|
383
|
+
const output: AppendLogResult = {
|
|
384
|
+
entry: decodeEnrichedStreamEntry(input.entry),
|
|
385
|
+
deduplicated: input.deduplicated,
|
|
386
|
+
};
|
|
381
387
|
if (input.routing !== undefined) {
|
|
382
388
|
output.routing = input.routing === null ? null : decodeRoutingEcho(input.routing);
|
|
383
389
|
}
|
package/src/protocol/errors.ts
CHANGED
|
@@ -14,6 +14,7 @@ export enum ErrorCode {
|
|
|
14
14
|
NOT_FOUND = 'NOT_FOUND',
|
|
15
15
|
REPOSITORY_ALREADY_ASSOCIATED = 'REPOSITORY_ALREADY_ASSOCIATED',
|
|
16
16
|
CUBE_ALREADY_ASSOCIATED = 'CUBE_ALREADY_ASSOCIATED',
|
|
17
|
+
POST_ID_CONFLICT = 'POST_ID_CONFLICT',
|
|
17
18
|
ROLE_IN_USE = 'ROLE_IN_USE',
|
|
18
19
|
DEFAULT_ROLE_REQUIRED = 'DEFAULT_ROLE_REQUIRED',
|
|
19
20
|
ROLE_REQUIRED = 'ROLE_REQUIRED',
|
package/src/protocol/types.ts
CHANGED
|
@@ -171,6 +171,7 @@ export interface RoutingEcho {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export interface AppendLogRequest {
|
|
174
|
+
post_id: string;
|
|
174
175
|
message: string;
|
|
175
176
|
visibility?: LogVisibility;
|
|
176
177
|
recipientDroneIds?: string[];
|
|
@@ -180,6 +181,7 @@ export interface AppendLogRequest {
|
|
|
180
181
|
|
|
181
182
|
export interface AppendLogResponse {
|
|
182
183
|
entry: ActivityLogEntry;
|
|
184
|
+
deduplicated: boolean;
|
|
183
185
|
routing?: RoutingEcho | null;
|
|
184
186
|
unreachableRecipients?: Array<{ id: string; label: string }>;
|
|
185
187
|
}
|
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 v9. */
|
|
2
|
+
export const PROTOCOL_VERSION = '9' as const;
|
|
3
3
|
|
|
4
4
|
export type ProtocolVersion = typeof PROTOCOL_VERSION;
|