borgmcp-shared 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/RELEASES.md +14 -0
- package/dist/conformance/adapter.d.ts +3 -0
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +91 -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 +83 -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 +89 -0
- package/docs/releases/0.12.0.md +7 -0
- package/docs/releasing.md +102 -20
- package/package.json +1 -1
- package/src/conformance/adapter.ts +110 -9
- package/src/conformance/index.ts +101 -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,
|
|
@@ -361,6 +362,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
361
362
|
{ id: 'security.adapter-boundary-injection', area: 'security' },
|
|
362
363
|
{ id: 'security.oversize-request', area: 'security' },
|
|
363
364
|
{ id: 'security.cross-cube-isolation', area: 'security' },
|
|
365
|
+
{ id: 'log.append-idempotency', area: 'log' },
|
|
364
366
|
{ id: 'log.read-cursor-tuple', area: 'cursor' },
|
|
365
367
|
{ id: 'sse.replay-live-transition', area: 'sse' },
|
|
366
368
|
{ id: 'cursor.explicit-expiry', area: 'cursor' },
|
|
@@ -1323,7 +1325,10 @@ export async function runAdapterConformance(
|
|
|
1323
1325
|
await record('security.adapter-boundary-injection', async () => {
|
|
1324
1326
|
const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
|
|
1325
1327
|
const injectedBody = JSON.stringify(
|
|
1326
|
-
createProtocolEnvelope('inject-b1', {
|
|
1328
|
+
createProtocolEnvelope('inject-b1', {
|
|
1329
|
+
post_id: '00000000-0000-4000-8000-000000000301',
|
|
1330
|
+
message: injectedMessage,
|
|
1331
|
+
}),
|
|
1327
1332
|
);
|
|
1328
1333
|
invariant(
|
|
1329
1334
|
utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
|
|
@@ -1342,7 +1347,10 @@ export async function runAdapterConformance(
|
|
|
1342
1347
|
const sentinel = await environment.operations.append(
|
|
1343
1348
|
credentialB,
|
|
1344
1349
|
cubeB,
|
|
1345
|
-
createProtocolEnvelope('inject-b2', {
|
|
1350
|
+
createProtocolEnvelope('inject-b2', {
|
|
1351
|
+
post_id: '00000000-0000-4000-8000-000000000302',
|
|
1352
|
+
message: 'post-injection-sentinel',
|
|
1353
|
+
}),
|
|
1346
1354
|
);
|
|
1347
1355
|
expectStatus(sentinel, 201, 'Post-injection sentinel append');
|
|
1348
1356
|
const read = await environment.operations.read(
|
|
@@ -1389,7 +1397,10 @@ export async function runAdapterConformance(
|
|
|
1389
1397
|
|
|
1390
1398
|
await record('security.oversize-request', async () => {
|
|
1391
1399
|
const baseBody = JSON.stringify(
|
|
1392
|
-
createProtocolEnvelope('oversize-a1', {
|
|
1400
|
+
createProtocolEnvelope('oversize-a1', {
|
|
1401
|
+
post_id: '00000000-0000-4000-8000-000000000303',
|
|
1402
|
+
message: 'must-not-persist',
|
|
1403
|
+
}),
|
|
1393
1404
|
);
|
|
1394
1405
|
const oversizedBody = baseBody + ' '.repeat(
|
|
1395
1406
|
Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
|
|
@@ -1417,7 +1428,10 @@ export async function runAdapterConformance(
|
|
|
1417
1428
|
const secretAppend = await environment.operations.append(
|
|
1418
1429
|
credentialB,
|
|
1419
1430
|
cubeB,
|
|
1420
|
-
createProtocolEnvelope('append-b1', {
|
|
1431
|
+
createProtocolEnvelope('append-b1', {
|
|
1432
|
+
post_id: '00000000-0000-4000-8000-000000000304',
|
|
1433
|
+
message: 'principal-b-secret',
|
|
1434
|
+
}),
|
|
1421
1435
|
);
|
|
1422
1436
|
expectStatus(secretAppend, 201, 'Principal B append');
|
|
1423
1437
|
const denied = await environment.operations.read(
|
|
@@ -1429,6 +1443,77 @@ export async function runAdapterConformance(
|
|
|
1429
1443
|
return { status: 404, code: ErrorCode.NOT_FOUND };
|
|
1430
1444
|
});
|
|
1431
1445
|
|
|
1446
|
+
await record('log.append-idempotency', async () => {
|
|
1447
|
+
const principal = await environment.admin.createPrincipal('append-idempotency');
|
|
1448
|
+
const otherPrincipal = await environment.admin.createPrincipal('append-idempotency-other');
|
|
1449
|
+
const cube = await environment.admin.createCube('append-idempotency');
|
|
1450
|
+
await environment.admin.grantCube(principal, cube);
|
|
1451
|
+
await environment.admin.grantCube(otherPrincipal, cube);
|
|
1452
|
+
const credential = await environment.admin.issueDroneSession(principal);
|
|
1453
|
+
const otherCredential = await environment.admin.issueDroneSession(otherPrincipal);
|
|
1454
|
+
const role = await environment.admin.createRole(cube, {
|
|
1455
|
+
roleClass: 'worker',
|
|
1456
|
+
isHumanSeat: false,
|
|
1457
|
+
});
|
|
1458
|
+
const firstRecipient = await environment.admin.createDrone(principal, cube, role);
|
|
1459
|
+
const secondRecipient = await environment.admin.createDrone(principal, cube, role);
|
|
1460
|
+
const postId = '00000000-0000-4000-8000-000000000313';
|
|
1461
|
+
const initialPayload = {
|
|
1462
|
+
post_id: postId,
|
|
1463
|
+
message: 'once',
|
|
1464
|
+
visibility: 'direct' as const,
|
|
1465
|
+
recipientDroneIds: [firstRecipient.id],
|
|
1466
|
+
class: 'review',
|
|
1467
|
+
to: ['Coordinator'],
|
|
1468
|
+
};
|
|
1469
|
+
const first = await environment.operations.append(
|
|
1470
|
+
credential,
|
|
1471
|
+
cube,
|
|
1472
|
+
createProtocolEnvelope('append-idempotency-first', initialPayload),
|
|
1473
|
+
);
|
|
1474
|
+
expectStatus(first, 201, 'Initial idempotent append');
|
|
1475
|
+
const firstResult = decodeAppendLogResultEnvelope(first.body).payload;
|
|
1476
|
+
invariant(!firstResult.deduplicated, 'Initial append was reported as deduplicated.');
|
|
1477
|
+
for (const [index, vector] of APPEND_LOG_IDEMPOTENCY_CONFORMANCE.entries()) {
|
|
1478
|
+
const payload = {
|
|
1479
|
+
...initialPayload,
|
|
1480
|
+
...(vector.mutation === 'message' ? { message: 'changed' } : {}),
|
|
1481
|
+
...(vector.mutation === 'visibility' ? { visibility: 'broadcast' as const } : {}),
|
|
1482
|
+
...(vector.mutation === 'recipient_set' ? { recipientDroneIds: [secondRecipient.id] } : {}),
|
|
1483
|
+
...(vector.mutation === 'resolved_class_routing' ? { class: 'security', to: ['Security Auditor'] } : {}),
|
|
1484
|
+
};
|
|
1485
|
+
const response = await environment.operations.append(
|
|
1486
|
+
vector.actor === 'same' ? credential : otherCredential,
|
|
1487
|
+
cube,
|
|
1488
|
+
createProtocolEnvelope(`append-idempotency-${index}`, payload),
|
|
1489
|
+
);
|
|
1490
|
+
if (vector.expected === 'POST_ID_CONFLICT') {
|
|
1491
|
+
expectError(response, 409, ErrorCode.POST_ID_CONFLICT, vector.name);
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
expectStatus(response, 201, vector.name);
|
|
1495
|
+
const result = decodeAppendLogResultEnvelope(response.body).payload;
|
|
1496
|
+
invariant(result.deduplicated === (vector.expected === 'deduplicated'), `${vector.name} returned the wrong deduplicated flag.`);
|
|
1497
|
+
invariant(
|
|
1498
|
+
vector.expected === 'deduplicated'
|
|
1499
|
+
? result.entry.id === firstResult.entry.id
|
|
1500
|
+
: result.entry.id !== firstResult.entry.id,
|
|
1501
|
+
`${vector.name} returned the wrong entry identity.`,
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const read = await environment.operations.read(
|
|
1505
|
+
credential,
|
|
1506
|
+
cube,
|
|
1507
|
+
createProtocolEnvelope('append-idempotency-read', { cursor: null, limit: 10 }),
|
|
1508
|
+
);
|
|
1509
|
+
expectStatus(read, 200, 'Idempotent append read');
|
|
1510
|
+
invariant(
|
|
1511
|
+
decodeReadLogResultEnvelope(read.body).payload.entries.length === 2,
|
|
1512
|
+
'Append collision controls persisted the wrong number of entries.',
|
|
1513
|
+
);
|
|
1514
|
+
return { persisted_entries: 2, stable_entry: true, deduplicated: true, conflicts: 4, cross_author_created: true };
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1432
1517
|
const entries: Array<{ id: string; created_at: string; message: string }> = [];
|
|
1433
1518
|
let readCursor: LogCursor | null = null;
|
|
1434
1519
|
await record('log.read-cursor-tuple', async () => {
|
|
@@ -1436,7 +1521,10 @@ export async function runAdapterConformance(
|
|
|
1436
1521
|
const response = await environment.operations.append(
|
|
1437
1522
|
credentialA,
|
|
1438
1523
|
cubeA,
|
|
1439
|
-
createProtocolEnvelope(`append-a${index + 1}`, {
|
|
1524
|
+
createProtocolEnvelope(`append-a${index + 1}`, {
|
|
1525
|
+
post_id: `00000000-0000-4000-8000-${String(305 + index).padStart(12, '0')}`,
|
|
1526
|
+
message,
|
|
1527
|
+
}),
|
|
1440
1528
|
);
|
|
1441
1529
|
expectStatus(response, 201, `Append ${message}`);
|
|
1442
1530
|
const entry = decodeAppendLogResultEnvelope(response.body).payload.entry;
|
|
@@ -1470,7 +1558,10 @@ export async function runAdapterConformance(
|
|
|
1470
1558
|
const appendDelta = environment.operations.append(
|
|
1471
1559
|
credentialA,
|
|
1472
1560
|
cubeA,
|
|
1473
|
-
createProtocolEnvelope('append-a4', {
|
|
1561
|
+
createProtocolEnvelope('append-a4', {
|
|
1562
|
+
post_id: '00000000-0000-4000-8000-000000000308',
|
|
1563
|
+
message: 'delta',
|
|
1564
|
+
}),
|
|
1474
1565
|
);
|
|
1475
1566
|
expectStatus(await appendDelta, 201, 'Transition append');
|
|
1476
1567
|
} finally {
|
|
@@ -1494,7 +1585,10 @@ export async function runAdapterConformance(
|
|
|
1494
1585
|
const epsilonResponse = await environment.operations.append(
|
|
1495
1586
|
credentialA,
|
|
1496
1587
|
cubeA,
|
|
1497
|
-
createProtocolEnvelope('append-a5', {
|
|
1588
|
+
createProtocolEnvelope('append-a5', {
|
|
1589
|
+
post_id: '00000000-0000-4000-8000-000000000309',
|
|
1590
|
+
message: 'epsilon',
|
|
1591
|
+
}),
|
|
1498
1592
|
);
|
|
1499
1593
|
expectStatus(epsilonResponse, 201, 'Live append');
|
|
1500
1594
|
const epsilon = logEvent(await within(noDuplicate, 'Live epsilon event', streamDeadlineMs), 'Live append');
|
|
@@ -2014,6 +2108,7 @@ export async function runAdapterConformance(
|
|
|
2014
2108
|
cubeA,
|
|
2015
2109
|
createProtocolEnvelope('evict-direct-target', {
|
|
2016
2110
|
message: 'must-not-fan-out',
|
|
2111
|
+
post_id: '00000000-0000-4000-8000-000000000310',
|
|
2017
2112
|
visibility: 'direct',
|
|
2018
2113
|
recipientDroneIds: [evictedDrone.id],
|
|
2019
2114
|
}),
|
|
@@ -2203,7 +2298,10 @@ export async function runAdapterConformance(
|
|
|
2203
2298
|
const attributed = await environment.operations.append(
|
|
2204
2299
|
evictedSession,
|
|
2205
2300
|
roleContractCube,
|
|
2206
|
-
createProtocolEnvelope('role-delete-attribution', {
|
|
2301
|
+
createProtocolEnvelope('role-delete-attribution', {
|
|
2302
|
+
post_id: '00000000-0000-4000-8000-000000000311',
|
|
2303
|
+
message: 'attribution survives',
|
|
2304
|
+
}),
|
|
2207
2305
|
);
|
|
2208
2306
|
expectStatus(attributed, 201, 'Pre-delete attributed log');
|
|
2209
2307
|
const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
|
|
@@ -2867,7 +2965,10 @@ export async function runAdapterConformance(
|
|
|
2867
2965
|
const append = await environment.operations.append(
|
|
2868
2966
|
creator.credential,
|
|
2869
2967
|
cube,
|
|
2870
|
-
createProtocolEnvelope('delete-log', {
|
|
2968
|
+
createProtocolEnvelope('delete-log', {
|
|
2969
|
+
post_id: '00000000-0000-4000-8000-000000000312',
|
|
2970
|
+
message: 'deleted with cube',
|
|
2971
|
+
}),
|
|
2871
2972
|
);
|
|
2872
2973
|
expectStatus(append, 201, 'Deletion fixture log');
|
|
2873
2974
|
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,53 @@ 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' | '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: 'rejects a same-author resolved class-routing collision', actor: 'same', mutation: 'resolved_class_routing', expected: 'POST_ID_CONFLICT' },
|
|
122
|
+
{ name: 'creates an independent cross-author post', actor: 'different', mutation: 'none', expected: 'created' },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const APPEND_LOG_REQUEST = {
|
|
126
|
+
post_id: '00000000-0000-4000-8000-000000000205',
|
|
127
|
+
message: 'REVIEW-READY: example',
|
|
128
|
+
} satisfies AppendLogRequest;
|
|
129
|
+
|
|
130
|
+
export const APPEND_LOG_REQUEST_CONFORMANCE: readonly AppendLogRequestConformanceVector[] = [
|
|
131
|
+
{ name: 'accepts a canonical post UUID', request: APPEND_LOG_REQUEST, accepts: true },
|
|
132
|
+
{ name: 'rejects an omitted post UUID', request: { message: APPEND_LOG_REQUEST.message }, accepts: false },
|
|
133
|
+
{ name: 'rejects an invalid post UUID', request: { ...APPEND_LOG_REQUEST, post_id: 'not-a-uuid' }, accepts: false },
|
|
134
|
+
{ name: 'rejects unknown request fields', request: { ...APPEND_LOG_REQUEST, extra: true }, accepts: false },
|
|
135
|
+
];
|
|
136
|
+
|
|
102
137
|
/** Runtime response vectors keep the append-log decoder aligned with its public type. */
|
|
103
138
|
export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceVector[] = [
|
|
104
139
|
{
|
|
105
140
|
name: 'accepts a response without optional routing metadata',
|
|
106
|
-
response: { entry: APPEND_LOG_ENTRY },
|
|
141
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false },
|
|
107
142
|
accepts: true,
|
|
108
143
|
},
|
|
109
144
|
{
|
|
110
145
|
name: 'accepts exact routing metadata and unreachable recipients',
|
|
111
146
|
response: {
|
|
112
147
|
entry: APPEND_LOG_ENTRY,
|
|
148
|
+
deduplicated: false,
|
|
113
149
|
routing: APPEND_LOG_ROUTING,
|
|
114
150
|
unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer' }],
|
|
115
151
|
},
|
|
@@ -117,73 +153,84 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
|
|
|
117
153
|
},
|
|
118
154
|
{
|
|
119
155
|
name: 'accepts a null routing echo and an empty unreachable-recipient list',
|
|
120
|
-
response: { entry: APPEND_LOG_ENTRY, routing: null, unreachableRecipients: [] },
|
|
156
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: true, routing: null, unreachableRecipients: [] },
|
|
121
157
|
accepts: true,
|
|
122
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: 'rejects an omitted deduplication result',
|
|
161
|
+
response: { entry: APPEND_LOG_ENTRY },
|
|
162
|
+
accepts: false,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'rejects a non-boolean deduplication result',
|
|
166
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: 'false' },
|
|
167
|
+
accepts: false,
|
|
168
|
+
},
|
|
123
169
|
{
|
|
124
170
|
name: 'rejects unknown top-level response fields',
|
|
125
|
-
response: { entry: APPEND_LOG_ENTRY, routing: APPEND_LOG_ROUTING, extra: true },
|
|
171
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: APPEND_LOG_ROUTING, extra: true },
|
|
126
172
|
accepts: false,
|
|
127
173
|
},
|
|
128
174
|
{
|
|
129
175
|
name: 'rejects a non-object routing echo',
|
|
130
|
-
response: { entry: APPEND_LOG_ENTRY, routing: 'review' },
|
|
176
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: 'review' },
|
|
131
177
|
accepts: false,
|
|
132
178
|
},
|
|
133
179
|
{
|
|
134
180
|
name: 'rejects missing routing fields',
|
|
135
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { class: 'review' } },
|
|
181
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { class: 'review' } },
|
|
136
182
|
accepts: false,
|
|
137
183
|
},
|
|
138
184
|
{
|
|
139
185
|
name: 'rejects unknown routing fields',
|
|
140
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, extra: true } },
|
|
186
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, extra: true } },
|
|
141
187
|
accepts: false,
|
|
142
188
|
},
|
|
143
189
|
{
|
|
144
190
|
name: 'rejects an invalid routing class',
|
|
145
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
|
|
191
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
|
|
146
192
|
accepts: false,
|
|
147
193
|
},
|
|
148
194
|
{
|
|
149
195
|
name: 'rejects an invalid routing recipient collection',
|
|
150
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
|
|
196
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
|
|
151
197
|
accepts: false,
|
|
152
198
|
},
|
|
153
199
|
{
|
|
154
200
|
name: 'rejects invalid routing recipient members',
|
|
155
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
|
|
201
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
|
|
156
202
|
accepts: false,
|
|
157
203
|
},
|
|
158
204
|
{
|
|
159
205
|
name: 'rejects an invalid routing fell-open flag',
|
|
160
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
|
|
206
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
|
|
161
207
|
accepts: false,
|
|
162
208
|
},
|
|
163
209
|
{
|
|
164
210
|
name: 'rejects an invalid routing message',
|
|
165
|
-
response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
|
|
211
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
|
|
166
212
|
accepts: false,
|
|
167
213
|
},
|
|
168
214
|
{
|
|
169
215
|
name: 'rejects a non-array unreachable-recipient list',
|
|
170
|
-
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: {} },
|
|
216
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: {} },
|
|
171
217
|
accepts: false,
|
|
172
218
|
},
|
|
173
219
|
{
|
|
174
220
|
name: 'rejects a non-object unreachable recipient',
|
|
175
|
-
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: ['missing-reviewer'] },
|
|
221
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: ['missing-reviewer'] },
|
|
176
222
|
accepts: false,
|
|
177
223
|
},
|
|
178
224
|
{
|
|
179
225
|
name: 'rejects missing unreachable-recipient fields',
|
|
180
|
-
response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: [{ id: 'missing-reviewer' }] },
|
|
226
|
+
response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: [{ id: 'missing-reviewer' }] },
|
|
181
227
|
accepts: false,
|
|
182
228
|
},
|
|
183
229
|
{
|
|
184
230
|
name: 'rejects unknown unreachable-recipient fields',
|
|
185
231
|
response: {
|
|
186
232
|
entry: APPEND_LOG_ENTRY,
|
|
233
|
+
deduplicated: false,
|
|
187
234
|
unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer', extra: true }],
|
|
188
235
|
},
|
|
189
236
|
accepts: false,
|
|
@@ -192,6 +239,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
|
|
|
192
239
|
name: 'rejects an invalid unreachable-recipient id',
|
|
193
240
|
response: {
|
|
194
241
|
entry: APPEND_LOG_ENTRY,
|
|
242
|
+
deduplicated: false,
|
|
195
243
|
unreachableRecipients: [{ id: 7, label: 'Missing Reviewer' }],
|
|
196
244
|
},
|
|
197
245
|
accepts: false,
|
|
@@ -200,6 +248,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
|
|
|
200
248
|
name: 'rejects an invalid unreachable-recipient label',
|
|
201
249
|
response: {
|
|
202
250
|
entry: APPEND_LOG_ENTRY,
|
|
251
|
+
deduplicated: false,
|
|
203
252
|
unreachableRecipients: [{ id: 'missing-reviewer', label: null }],
|
|
204
253
|
},
|
|
205
254
|
accepts: false,
|
|
@@ -750,11 +799,48 @@ const ATTACH_RESPONSE = {
|
|
|
750
799
|
runtime_metadata_reported: false,
|
|
751
800
|
},
|
|
752
801
|
session: { id: '40000000-0000-4000-8000-000000000001' },
|
|
802
|
+
initial_log_cursor: null,
|
|
753
803
|
} satisfies AttachResponse;
|
|
754
804
|
|
|
755
805
|
/** Wire vectors for the v3 non-expiring attach-session response. */
|
|
756
806
|
export const ATTACH_SESSION_CONFORMANCE: readonly AttachSessionConformanceVector[] = [
|
|
757
807
|
{ name: 'accepts exact non-expiring session id', response: ATTACH_RESPONSE, accepts: true },
|
|
808
|
+
{
|
|
809
|
+
name: 'accepts a canonical initial log cursor',
|
|
810
|
+
response: {
|
|
811
|
+
...ATTACH_RESPONSE,
|
|
812
|
+
initial_log_cursor: {
|
|
813
|
+
id: '50000000-0000-4000-8000-000000000001',
|
|
814
|
+
created_at: '2026-07-14T10:00:00.000Z',
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
accepts: true,
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: 'rejects an omitted initial log cursor',
|
|
821
|
+
response: (({ initial_log_cursor: _, ...response }) => response)(ATTACH_RESPONSE),
|
|
822
|
+
accepts: false,
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: 'rejects an invalid initial log cursor UUID',
|
|
826
|
+
response: {
|
|
827
|
+
...ATTACH_RESPONSE,
|
|
828
|
+
initial_log_cursor: { id: 'not-a-uuid', created_at: '2026-07-14T10:00:00.000Z' },
|
|
829
|
+
},
|
|
830
|
+
accepts: false,
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: 'rejects unknown initial log cursor fields',
|
|
834
|
+
response: {
|
|
835
|
+
...ATTACH_RESPONSE,
|
|
836
|
+
initial_log_cursor: {
|
|
837
|
+
id: '50000000-0000-4000-8000-000000000001',
|
|
838
|
+
created_at: '2026-07-14T10:00:00.000Z',
|
|
839
|
+
extra: true,
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
accepts: false,
|
|
843
|
+
},
|
|
758
844
|
{
|
|
759
845
|
name: 'rejects retired expires_at field',
|
|
760
846
|
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.0' 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;
|