borgmcp 3.15.1 → 3.16.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.
Files changed (58) hide show
  1. package/README.md +2 -0
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/dist/codex-launch.d.ts.map +1 -1
  4. package/dist/codex-launch.js +3 -2
  5. package/dist/codex-launch.js.map +1 -1
  6. package/dist/docs-sections.d.ts.map +1 -1
  7. package/dist/docs-sections.js +8 -0
  8. package/dist/docs-sections.js.map +1 -1
  9. package/dist/document-render.d.ts +6 -0
  10. package/dist/document-render.d.ts.map +1 -0
  11. package/dist/document-render.js +34 -0
  12. package/dist/document-render.js.map +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +33 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/launch-all-cmd.d.ts.map +1 -1
  17. package/dist/launch-all-cmd.js +12 -0
  18. package/dist/launch-all-cmd.js.map +1 -1
  19. package/dist/launch-all-deps.d.ts +2 -0
  20. package/dist/launch-all-deps.d.ts.map +1 -1
  21. package/dist/launch-all-deps.js +13 -0
  22. package/dist/launch-all-deps.js.map +1 -1
  23. package/dist/log-stream.d.ts +2 -1
  24. package/dist/log-stream.d.ts.map +1 -1
  25. package/dist/log-stream.js +16 -3
  26. package/dist/log-stream.js.map +1 -1
  27. package/dist/regen-format.d.ts.map +1 -1
  28. package/dist/regen-format.js +6 -1
  29. package/dist/regen-format.js.map +1 -1
  30. package/dist/remote-client.d.ts +6 -1
  31. package/dist/remote-client.d.ts.map +1 -1
  32. package/dist/remote-client.js +41 -7
  33. package/dist/remote-client.js.map +1 -1
  34. package/dist/server-handshake.d.ts +4 -3
  35. package/dist/server-handshake.d.ts.map +1 -1
  36. package/dist/server-handshake.js.map +1 -1
  37. package/dist/tool-manifest.d.ts.map +1 -1
  38. package/dist/tool-manifest.js +65 -2
  39. package/dist/tool-manifest.js.map +1 -1
  40. package/dist/update-cmd.d.ts +5 -1
  41. package/dist/update-cmd.d.ts.map +1 -1
  42. package/dist/update-cmd.js +54 -14
  43. package/dist/update-cmd.js.map +1 -1
  44. package/docs/DOCUMENTS.md +49 -0
  45. package/docs/RELEASING.md +2 -2
  46. package/package.json +2 -2
  47. package/src/codex-launch.ts +3 -2
  48. package/src/docs-sections.ts +8 -0
  49. package/src/document-render.ts +41 -0
  50. package/src/index.ts +44 -1
  51. package/src/launch-all-cmd.ts +14 -0
  52. package/src/launch-all-deps.ts +15 -0
  53. package/src/log-stream.ts +21 -3
  54. package/src/regen-format.ts +6 -1
  55. package/src/remote-client.ts +108 -15
  56. package/src/server-handshake.ts +4 -3
  57. package/src/tool-manifest.ts +69 -2
  58. package/src/update-cmd.ts +67 -17
package/src/index.ts CHANGED
@@ -52,8 +52,17 @@ import {
52
52
  applyTemplate,
53
53
  whoami,
54
54
  roleRationale,
55
+ putDocument,
56
+ getDocument,
57
+ listDocuments,
58
+ removeDocument,
55
59
  type LocalManageAuthority,
56
60
  } from './remote-client.js';
61
+ import {
62
+ formatDocument,
63
+ formatDocumentCitations,
64
+ formatDocumentMetadata,
65
+ } from './document-render.js';
57
66
  import {
58
67
  getTemplate,
59
68
  listTemplateNames,
@@ -840,6 +849,33 @@ export async function main() {
840
849
  return { content: [{ type: 'text', text: lines.join('\n') }] };
841
850
  }
842
851
 
852
+ case 'borg_put-document': {
853
+ const active = await requireActiveCube();
854
+ const result = await putDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
855
+ return { content: [{ type: 'text', text: `Created cube document.\n\n${formatDocument(result.document)}` }] };
856
+ }
857
+
858
+ case 'borg_get-document': {
859
+ const active = await requireActiveCube();
860
+ const result = await getDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
861
+ return { content: [{ type: 'text', text: formatDocument(result.document) }] };
862
+ }
863
+
864
+ case 'borg_list-documents': {
865
+ const active = await requireActiveCube();
866
+ const result = await listDocuments(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
867
+ const text = result.documents.length === 0
868
+ ? `No active or superseded documents in cube "${active.name}".`
869
+ : result.documents.map(formatDocumentMetadata).join('\n\n');
870
+ return { content: [{ type: 'text', text }] };
871
+ }
872
+
873
+ case 'borg_remove-document': {
874
+ const active = await requireActiveCube();
875
+ const result = await removeDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
876
+ return { content: [{ type: 'text', text: `Removed cube document.\n\n${formatDocumentMetadata(result.document)}` }] };
877
+ }
878
+
843
879
  case 'borg_log': {
844
880
  const message = args?.message as string;
845
881
  if (!message || typeof message !== 'string') throw new Error('message is required');
@@ -870,6 +906,7 @@ export async function main() {
870
906
  args?.visibility === 'broadcast' || args?.visibility === 'direct'
871
907
  ? args.visibility
872
908
  : undefined;
909
+ const documents = args?.documents as string[] | undefined;
873
910
  if (!active.serverTrustIdentity) {
874
911
  throw new Error('Selected Borg server authority state is missing or unreadable');
875
912
  }
@@ -877,6 +914,7 @@ export async function main() {
877
914
  ...(explicitClass ? { class: explicitClass } : {}),
878
915
  ...(hasTo ? { to: recipients ?? [] } : {}),
879
916
  ...(visibility ? { visibility } : {}),
917
+ ...(documents ? { documents } : {}),
880
918
  serverTrustIdentity: active.serverTrustIdentity,
881
919
  };
882
920
  const result = await appendLog(active.sessionToken, active.apiUrl, message, appendOpts);
@@ -891,7 +929,12 @@ export async function main() {
891
929
  .map((r) => r.label)
892
930
  .join(', ')}. Message delivered — they'll read it when they return.`
893
931
  : '';
894
- const text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${echo}${unreachable}`;
932
+ const cited = formatDocumentCitations(result.entry.documents);
933
+ const citations = cited.length > 0 ? `\nDocuments: ${cited.join('; ')}` : '';
934
+ const advisory = result.advisory?.code === 'STORE_AS_DOCUMENT'
935
+ ? `\nAdvisory: this message exceeded ${result.advisory.threshold_bytes} UTF-8 bytes. Store durable detail with borg_put-document and cite its full id in borg_log.documents.`
936
+ : '';
937
+ const text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${echo}${unreachable}${citations}${advisory}`;
895
938
  return { content: [{ type: 'text', text }] };
896
939
  }
897
940
 
@@ -320,6 +320,20 @@ export async function runLaunchAll(
320
320
  return 0;
321
321
  }
322
322
 
323
+ // Confirm the one configured authority is accepting pinned protocol requests
324
+ // before any agent process starts. Per-seat checks below remain authoritative
325
+ // for each saved session only after this fleet-wide readiness boundary passes.
326
+ try {
327
+ await deps.probeAuthority(lockLaunchable[0].seat);
328
+ } catch (error) {
329
+ const reason = error instanceof Error ? error.message : String(error);
330
+ deps.stderr(
331
+ `borg launch-all: configured Borg authority is unavailable (${reason}); ` +
332
+ 'nothing was launched. Ask the server operator to restore it, then retry.\n',
333
+ );
334
+ return 1;
335
+ }
336
+
323
337
  // 4b. server-liveness skip — drop seats the server reports EVICTED (gone).
324
338
  // Reuses the gh#882 per-seat probe (each seat's OWN token → 410
325
339
  // DRONE_EVICTED). Relaunching an evicted seat silently re-mints a fresh
@@ -26,6 +26,9 @@ import {
26
26
  import { getRoster, getCube } from './remote-client.js';
27
27
  import { defaultProbeSeat, type SeatStatus } from './seat-probe.js';
28
28
  import { borgHomeRoot } from './private-root.js';
29
+ import { preflightBorgServerTag } from './server-handshake.js';
30
+ import { loadBorgServerTrust } from './server-trust.js';
31
+ import { BorgServerTrustError } from './server-errors.js';
29
32
 
30
33
  /** Subprocess runner — sync, returns stdout, THROWS on non-zero exit or ENOENT. */
31
34
  export type RunSyncFn = (cmd: string, args: string[]) => string;
@@ -71,6 +74,8 @@ export interface LaunchAllDeps {
71
74
  token: string,
72
75
  cubeId: string
73
76
  ) => Promise<{ id: string; name: string; roles: Array<{ id: string; name: string }> }>;
77
+ /** Verify the configured pinned authority is accepting protocol requests. */
78
+ probeAuthority: (seat: ActiveCube) => Promise<void>;
74
79
  /**
75
80
  * Probe ONE saved seat's server-side liveness using ITS OWN token (gh#877
76
81
  * reuse via seat-probe.ts). Lets launch-all skip evicted seats instead
@@ -156,6 +161,16 @@ export function buildDefaultLaunchAllDeps(): LaunchAllDeps {
156
161
  getRoster: (seat, since) => getRoster(seat, since),
157
162
  // getCube uses the drone session token via authedFetch (cubeId-only); apiUrl/token unused.
158
163
  getCube: (_apiUrl, _token, cubeId) => getCube(cubeId),
164
+ probeAuthority: async (seat) => {
165
+ if (!seat.serverTrustIdentity) {
166
+ throw new BorgServerTrustError('Saved Borg server trust identity is missing');
167
+ }
168
+ const trust = await loadBorgServerTrust(seat.apiUrl);
169
+ if (trust.identity !== seat.serverTrustIdentity) {
170
+ throw new BorgServerTrustError('Saved Borg server trust identity changed');
171
+ }
172
+ await preflightBorgServerTag(seat.apiUrl, trust.fetchImpl);
173
+ },
159
174
  probeSeat: (seat) => defaultProbeSeat(seat),
160
175
  getCliPreferenceForPath: (projectPath) => getProjectCliPreferenceForPath(projectPath),
161
176
  readAllProjectIdentities: () => cubesReadAllProjectIdentities(),
package/src/log-stream.ts CHANGED
@@ -28,7 +28,12 @@ import { Buffer } from 'node:buffer';
28
28
  import { promises as fs } from 'node:fs';
29
29
  import path from 'node:path';
30
30
  import { compareBroadcastHwm, type BroadcastHwm } from 'borgmcp-shared/log-stream-hwm';
31
- import { decodeProtocolErrorEnvelope, ErrorCode } from 'borgmcp-shared/protocol';
31
+ import {
32
+ decodeDocumentCitations,
33
+ decodeProtocolErrorEnvelope,
34
+ ErrorCode,
35
+ type DocumentCitation,
36
+ } from 'borgmcp-shared/protocol';
32
37
  import { getActiveCube, inboxPathForDrone } from './cubes.js';
33
38
  import { assertUuidShape } from './evict-drone.js';
34
39
  import { loadBorgServerTrust } from './server-trust.js';
@@ -59,6 +64,7 @@ import { formatCubeActivityWakeMessage } from './cube-activity-wake-copy.js';
59
64
  import { readBoundedResponseBody } from './server-response.js';
60
65
  import { BorgServerError } from './server-errors.js';
61
66
  import { markSeatRejected } from './seats.js';
67
+ import { formatDocumentCitations } from './document-render.js';
62
68
  import { hasPendingWakeEntry as hasPendingDurableWakeEntry } from './remote-client.js';
63
69
  import {
64
70
  acquireStreamLease,
@@ -1426,7 +1432,14 @@ function parseEventBlock(block: string): ParsedEvent | null {
1426
1432
  const validCursor = cursor &&
1427
1433
  typeof cursor.id === 'string' &&
1428
1434
  typeof cursor.created_at === 'string';
1429
- const entry = parsed?.entry ?? parsed;
1435
+ let entry = parsed?.entry ?? parsed;
1436
+ if (entry?.documents !== undefined) {
1437
+ try {
1438
+ entry = { ...entry, documents: decodeDocumentCitations(entry.documents) };
1439
+ } catch {
1440
+ return { type: 'unknown', raw: block };
1441
+ }
1442
+ }
1430
1443
  return {
1431
1444
  type: 'log',
1432
1445
  id,
@@ -1493,6 +1506,7 @@ export interface EnrichedEntry {
1493
1506
  drone_label?: string | null;
1494
1507
  role_name?: string | null;
1495
1508
  message?: string;
1509
+ documents?: DocumentCitation[];
1496
1510
  }
1497
1511
 
1498
1512
  function parseHeartbeatHwm(value: unknown): BroadcastHwm | null {
@@ -1566,7 +1580,11 @@ export function formatInboxLine(entry: EnrichedEntry): string {
1566
1580
  const idPrefix = entryId ? `[entry_id: ${entryId}] ` : '';
1567
1581
  // Normalize \r\n, \r, and \n all to ` ⏎ ` so the entry fits on one
1568
1582
  // physical line regardless of line-ending convention in the source.
1569
- const message = rawMessage.replace(/\r\n|\r|\n/g, ' ⏎ ');
1583
+ const citations = formatDocumentCitations(entry.documents);
1584
+ const withDocuments = citations.length > 0
1585
+ ? `${rawMessage}\nDocuments: ${citations.join('; ')}`
1586
+ : rawMessage;
1587
+ const message = withDocuments.replace(/\r\n|\r|\n/g, ' ⏎ ');
1570
1588
  return `${ts} ${label} (${role}): ${idPrefix}${message}`;
1571
1589
  }
1572
1590
 
@@ -17,6 +17,7 @@ import {
17
17
  RUNTIME_METADATA_ADVISORY,
18
18
  renderRuntimeMetadataLines,
19
19
  } from './roster-render.js';
20
+ import { formatDocumentCitations } from './document-render.js';
20
21
  import { shellEscape } from './shell-escape.js';
21
22
  import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
22
23
  import { isBorgSession } from './launch-gate.js';
@@ -697,5 +698,9 @@ export function formatLogEntryMarkdown(
697
698
  typeof entry.drone_id === 'string' && entry.drone_id.length > 0
698
699
  ? ` ${formatDroneAddressToken(entry.drone_id)}`
699
700
  : '';
700
- return `**[${ts}]**${entryId}${addr} ${d?.label ?? '?'} (${r?.name ?? '?'}): ${entry.message}`;
701
+ const citations = formatDocumentCitations(entry.documents);
702
+ const documents = citations.length > 0
703
+ ? `\n Documents:\n${citations.map((citation) => ` - ${citation}`).join('\n')}`
704
+ : '';
705
+ return `**[${ts}]**${entryId}${addr} ${d?.label ?? '?'} (${r?.name ?? '?'}): ${entry.message}${documents}`;
701
706
  }
@@ -16,6 +16,7 @@ import {
16
16
  import { randomUUID } from 'node:crypto';
17
17
  import {
18
18
  createProtocolEnvelope,
19
+ decodeAppendLogRequest,
19
20
  decodeAppendLogResult,
20
21
  decodeDeleteCubeResponse,
21
22
  decodeDeleteRoleRequest,
@@ -25,6 +26,15 @@ import {
25
26
  decodeProtocolEnvelope,
26
27
  decodeProtocolErrorEnvelope,
27
28
  decodeReassignDroneResult,
29
+ decodeReadLogResult,
30
+ decodePutDocumentRequest,
31
+ decodePutDocumentResult,
32
+ decodeGetDocumentRequest,
33
+ decodeGetDocumentResult,
34
+ decodeListDocumentsRequest,
35
+ decodeListDocumentsResult,
36
+ decodeRemoveDocumentRequest,
37
+ decodeRemoveDocumentResult,
28
38
  decodeRoleRationaleRequest,
29
39
  decodeRoleRationaleResult,
30
40
  decodeUpdateDroneRuntimeMetadataResponse,
@@ -35,6 +45,10 @@ import {
35
45
  type EvictDroneResult,
36
46
  type ReassignDroneResult,
37
47
  type RoleRationaleResult,
48
+ type PutDocumentResult,
49
+ type GetDocumentResult,
50
+ type ListDocumentsResult,
51
+ type RemoveDocumentResult,
38
52
  } from 'borgmcp-shared/protocol';
39
53
  import { consolePrefix } from './console-prefix.js';
40
54
  import { debugLog } from './debug.js';
@@ -315,7 +329,7 @@ async function decodeLocalProtocolResponse<T>(
315
329
  async function localServerRequest<T>(
316
330
  active: ActiveCube,
317
331
  path: string,
318
- method: 'GET' | 'POST' | 'PUT' | 'PATCH',
332
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
319
333
  payload?: Record<string, unknown>,
320
334
  options: {
321
335
  retryMode?: AuthedFetchRetryMode;
@@ -549,7 +563,7 @@ async function localReadLogPage(
549
563
  retryMode?: AuthedFetchRetryMode;
550
564
  } = {},
551
565
  ): Promise<any> {
552
- const payload = await localServerRequest<any>(
566
+ const payload = await localServerRequest(
553
567
  active,
554
568
  `/api/cubes/${active.cubeId}/logs`,
555
569
  'PUT',
@@ -557,7 +571,7 @@ async function localReadLogPage(
557
571
  cursor: opts.cursor ?? null,
558
572
  ...(opts.limit === undefined ? {} : { limit: opts.limit }),
559
573
  },
560
- { retryMode: opts.retryMode },
574
+ { retryMode: opts.retryMode, decodePayload: decodeReadLogResult },
561
575
  );
562
576
  if (!payload) throw new Error('Local Borg server returned an empty log response');
563
577
  return payload;
@@ -1321,6 +1335,82 @@ export async function roleRationale(
1321
1335
  };
1322
1336
  }
1323
1337
 
1338
+ export async function putDocument(
1339
+ sessionToken: string,
1340
+ apiUrl: string,
1341
+ input: unknown,
1342
+ serverTrustIdentity?: string,
1343
+ ): Promise<PutDocumentResult> {
1344
+ const request = decodePutDocumentRequest(input);
1345
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
1346
+ const result = await localServerRequest<PutDocumentResult>(
1347
+ local,
1348
+ `/api/cubes/${local.cubeId}/documents`,
1349
+ 'PUT',
1350
+ { ...request },
1351
+ { decodePayload: decodePutDocumentResult },
1352
+ );
1353
+ if (!result) throw new Error('Local Borg server returned an empty document response');
1354
+ return result;
1355
+ }
1356
+
1357
+ export async function getDocument(
1358
+ sessionToken: string,
1359
+ apiUrl: string,
1360
+ input: unknown,
1361
+ serverTrustIdentity?: string,
1362
+ ): Promise<GetDocumentResult> {
1363
+ const request = decodeGetDocumentRequest(input);
1364
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
1365
+ const result = await localServerRequest<GetDocumentResult>(
1366
+ local,
1367
+ `/api/cubes/${local.cubeId}/documents/${encodeURIComponent(request.id)}`,
1368
+ 'GET',
1369
+ { ...request },
1370
+ { decodePayload: decodeGetDocumentResult },
1371
+ );
1372
+ if (!result) throw new Error('Local Borg server returned an empty document response');
1373
+ return result;
1374
+ }
1375
+
1376
+ export async function listDocuments(
1377
+ sessionToken: string,
1378
+ apiUrl: string,
1379
+ input: unknown,
1380
+ serverTrustIdentity?: string,
1381
+ ): Promise<ListDocumentsResult> {
1382
+ decodeListDocumentsRequest(input);
1383
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
1384
+ const result = await localServerRequest<ListDocumentsResult>(
1385
+ local,
1386
+ `/api/cubes/${local.cubeId}/documents`,
1387
+ 'GET',
1388
+ {},
1389
+ { decodePayload: decodeListDocumentsResult },
1390
+ );
1391
+ if (!result) throw new Error('Local Borg server returned an empty document-list response');
1392
+ return result;
1393
+ }
1394
+
1395
+ export async function removeDocument(
1396
+ sessionToken: string,
1397
+ apiUrl: string,
1398
+ input: unknown,
1399
+ serverTrustIdentity?: string,
1400
+ ): Promise<RemoveDocumentResult> {
1401
+ const request = decodeRemoveDocumentRequest(input);
1402
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
1403
+ const result = await localServerRequest<RemoveDocumentResult>(
1404
+ local,
1405
+ `/api/cubes/${local.cubeId}/documents/${encodeURIComponent(request.id)}`,
1406
+ 'DELETE',
1407
+ { ...request },
1408
+ { decodePayload: decodeRemoveDocumentResult },
1409
+ );
1410
+ if (!result) throw new Error('Local Borg server returned an empty document response');
1411
+ return result;
1412
+ }
1413
+
1324
1414
  /**
1325
1415
  * Append a message to the cube's shared activity log.
1326
1416
  */
@@ -1333,6 +1423,7 @@ export async function appendLog(
1333
1423
  recipientDroneIds?: string[];
1334
1424
  class?: string;
1335
1425
  to?: string[];
1426
+ documents?: string[];
1336
1427
  serverTrustIdentity?: string;
1337
1428
  } = {}
1338
1429
  ): Promise<ReturnType<typeof decodeAppendLogResult>> {
@@ -1342,6 +1433,9 @@ export async function appendLog(
1342
1433
  'Remove visibility to direct to recipients, or remove to: to broadcast.',
1343
1434
  );
1344
1435
  }
1436
+ if (opts.to?.length === 0) {
1437
+ throw new Error('Direct log recipient list must contain at least one recipient');
1438
+ }
1345
1439
  const postId = randomUUID();
1346
1440
  const local = await localAuthorityContext(
1347
1441
  sessionToken,
@@ -1370,22 +1464,21 @@ export async function appendLog(
1370
1464
  } else if (visibility === undefined && recipientDroneIds !== undefined) {
1371
1465
  visibility = 'direct';
1372
1466
  }
1467
+ const request = decodeAppendLogRequest({
1468
+ post_id: postId,
1469
+ message,
1470
+ ...(visibility ? { visibility } : {}),
1471
+ ...(visibility === 'direct' && recipientDroneIds
1472
+ ? { recipientDroneIds }
1473
+ : {}),
1474
+ ...(opts.class ? { class: opts.class } : {}),
1475
+ ...(opts.documents ? { documents: opts.documents } : {}),
1476
+ });
1373
1477
  const payload = await localServerRequest<ReturnType<typeof decodeAppendLogResult>>(
1374
1478
  local,
1375
1479
  `/api/cubes/${local.cubeId}/logs`,
1376
1480
  'POST',
1377
- {
1378
- post_id: postId,
1379
- message,
1380
- ...(visibility ? { visibility } : {}),
1381
- ...(visibility === 'direct' && recipientDroneIds
1382
- ? { recipientDroneIds }
1383
- : {}),
1384
- // server#48 append-time taxonomy routing: forward the requested class
1385
- // so the server can classify/route. It is honored only when no explicit
1386
- // visibility/recipients override it (server resolveMessageRouting).
1387
- ...(opts.class ? { class: opts.class } : {}),
1388
- },
1481
+ { ...request },
1389
1482
  { retryMode: 'append-log', decodePayload: decodeAppendLogResult },
1390
1483
  );
1391
1484
  if (!payload) throw new Error('Local Borg server returned an empty log response');
@@ -215,10 +215,11 @@ export interface ServerAttachResult {
215
215
  }
216
216
 
217
217
  /**
218
- * Attach an enrolled client principal to one granted cube/role over protocol v9.
218
+ * Attach an enrolled client principal to one granted cube/role over protocol v10.
219
219
  * The client CSPRNG-generates the session bearer and persists it PENDING in the
220
- * OS keychain (keyed by the stable per-seat identity) BEFORE this request, so an
221
- * interrupted/lost response is recovered by re-sending the exact same bearer —
220
+ * local 0600 credential store (keyed by the stable per-seat identity) BEFORE
221
+ * this request, so an interrupted/lost response is recovered by re-sending the
222
+ * exact same bearer —
222
223
  * the server binds only its digest. A verified `created`/`reused` response
223
224
  * activates that pending record in place; the server never returns a bearer.
224
225
  */
@@ -7,7 +7,7 @@
7
7
  * CONTRACT-BACKED DATA — imports only published scalar contract constants, with
8
8
  * no client runtime side effects.
9
9
  */
10
- import { DECISION_TEXT_MAX_BYTES } from 'borgmcp-shared/protocol';
10
+ import { DECISION_TEXT_MAX_BYTES, DOCUMENT_CONTENT_TYPES } from 'borgmcp-shared/protocol';
11
11
 
12
12
  export interface ToolManifestEntry {
13
13
  name: string;
@@ -307,10 +307,69 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
307
307
  },
308
308
  },
309
309
  },
310
+ {
311
+ name: 'borg_put-document',
312
+ description:
313
+ 'Create an immutable cube document containing Markdown or plain text. Use this for durable material that is too large or detailed for an activity-log message. Pass `supersedes` with the full prior document id to create its next linear revision; content is never edited in place. Requires the selected local client to have a live cube-write or cube-manage grant.',
314
+ inputSchema: {
315
+ type: 'object',
316
+ properties: {
317
+ title: {
318
+ type: 'string',
319
+ maxLength: 120,
320
+ description: 'Document title, trimmed and control-free; max 120 Unicode characters and 480 UTF-8 bytes.',
321
+ },
322
+ content_type: {
323
+ type: 'string',
324
+ enum: [...DOCUMENT_CONTENT_TYPES],
325
+ description: 'Exact document content type: text/markdown or text/plain.',
326
+ },
327
+ content: {
328
+ type: 'string',
329
+ description: 'Immutable document content. The server enforces its configured UTF-8 byte limit.',
330
+ },
331
+ supersedes: {
332
+ type: 'string',
333
+ description: 'Optional full opaque id of the active document this new revision supersedes.',
334
+ },
335
+ },
336
+ required: ['title', 'content_type', 'content'],
337
+ },
338
+ },
339
+ {
340
+ name: 'borg_get-document',
341
+ description:
342
+ 'Fetch one cube document by its full opaque id, including immutable content, revision links, state, author, and removal audit metadata. Exact-id reads retain removed content for audit. Requires a live cube-read, cube-write, or cube-manage grant.',
343
+ inputSchema: {
344
+ type: 'object',
345
+ properties: {
346
+ id: { type: 'string', description: 'Full opaque document id. Do not abbreviate it.' },
347
+ },
348
+ required: ['id'],
349
+ },
350
+ },
351
+ {
352
+ name: 'borg_list-documents',
353
+ description:
354
+ 'List active and superseded document metadata in the current cube. Removed documents are omitted; use borg_get-document with an exact known id for retained audit content. Document bodies are not included.',
355
+ inputSchema: { type: 'object', properties: {}, required: [] },
356
+ },
357
+ {
358
+ name: 'borg_remove-document',
359
+ description:
360
+ 'Mark one cube document removed while retaining its immutable content and audit metadata. The server permits the document author or a client with a live cube-manage grant; workflow role labels grant no permission. Idempotent for an already removed document.',
361
+ inputSchema: {
362
+ type: 'object',
363
+ properties: {
364
+ id: { type: 'string', description: 'Full opaque document id to remove. Do not abbreviate it.' },
365
+ },
366
+ required: ['id'],
367
+ },
368
+ },
310
369
  {
311
370
  name: 'borg_log',
312
371
  description:
313
- 'Append a message to the cube\'s activity log. By default entries broadcast to all drones. When a cube declares a message taxonomy, borg_log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class:`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, the 8-hex short-uuid (the `id:` token shown in roster/read-log — a drone_id prefix that is STABLE across label renumber), role name, or role slug.',
372
+ 'Append a message to the cube\'s activity log. By default entries broadcast to all drones. Cite durable cube documents by passing their full ids in `documents`; citations carry current metadata but do not inline document content. When a cube declares a message taxonomy, borg_log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, the 8-hex short-uuid (the `id:` token shown in roster/read-log — a drone_id prefix that is STABLE across label renumber), role name, or role slug.',
314
373
  inputSchema: {
315
374
  type: 'object',
316
375
  properties: {
@@ -331,6 +390,14 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
331
390
  enum: ['broadcast', 'direct'],
332
391
  description: 'Optional explicit visibility. Overrides class-based routing defaults.',
333
392
  },
393
+ documents: {
394
+ type: 'array',
395
+ items: { type: 'string' },
396
+ minItems: 1,
397
+ maxItems: 100,
398
+ uniqueItems: true,
399
+ description: 'Optional full opaque ids of 1-100 same-cube documents to cite atomically. Unknown, duplicate, or foreign ids are refused.',
400
+ },
334
401
  },
335
402
  required: ['message'],
336
403
  },