borgmcp-shared 0.12.2 → 0.13.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 (59) hide show
  1. package/README.md +14 -0
  2. package/RELEASES.md +16 -0
  3. package/dist/conformance/adapter.d.ts +7 -0
  4. package/dist/conformance/adapter.d.ts.map +1 -1
  5. package/dist/conformance/adapter.js +157 -3
  6. package/dist/conformance/adapter.js.map +1 -1
  7. package/dist/conformance/index.d.ts +33 -0
  8. package/dist/conformance/index.d.ts.map +1 -1
  9. package/dist/conformance/index.js +10 -0
  10. package/dist/conformance/index.js.map +1 -1
  11. package/dist/protocol/contract.d.ts +36 -2
  12. package/dist/protocol/contract.d.ts.map +1 -1
  13. package/dist/protocol/contract.js +19 -5
  14. package/dist/protocol/contract.js.map +1 -1
  15. package/dist/protocol/coordination.d.ts.map +1 -1
  16. package/dist/protocol/coordination.js +11 -2
  17. package/dist/protocol/coordination.js.map +1 -1
  18. package/dist/protocol/documents.d.ts +78 -0
  19. package/dist/protocol/documents.d.ts.map +1 -0
  20. package/dist/protocol/documents.js +196 -0
  21. package/dist/protocol/documents.js.map +1 -0
  22. package/dist/protocol/errors.d.ts +5 -0
  23. package/dist/protocol/errors.d.ts.map +1 -1
  24. package/dist/protocol/errors.js +5 -0
  25. package/dist/protocol/errors.js.map +1 -1
  26. package/dist/protocol/index.d.ts +1 -0
  27. package/dist/protocol/index.d.ts.map +1 -1
  28. package/dist/protocol/index.js +1 -0
  29. package/dist/protocol/index.js.map +1 -1
  30. package/dist/protocol/sse.d.ts +2 -2
  31. package/dist/protocol/sse.d.ts.map +1 -1
  32. package/dist/protocol/sse.js +33 -4
  33. package/dist/protocol/sse.js.map +1 -1
  34. package/dist/protocol/types.d.ts +7 -0
  35. package/dist/protocol/types.d.ts.map +1 -1
  36. package/dist/protocol/version.d.ts +1 -1
  37. package/dist/protocol/version.d.ts.map +1 -1
  38. package/dist/protocol/version.js +1 -1
  39. package/dist/protocol/version.js.map +1 -1
  40. package/dist/templates.d.ts +1 -1
  41. package/dist/templates.d.ts.map +1 -1
  42. package/dist/templates.js +8 -4
  43. package/dist/templates.js.map +1 -1
  44. package/docs/compatibility.md +5 -0
  45. package/docs/cube-documents.md +35 -0
  46. package/docs/release-records.json +29 -0
  47. package/docs/releases/0.13.0.md +7 -0
  48. package/package.json +1 -1
  49. package/src/conformance/adapter.ts +340 -1
  50. package/src/conformance/index.ts +11 -0
  51. package/src/protocol/contract.ts +19 -5
  52. package/src/protocol/coordination.ts +15 -1
  53. package/src/protocol/documents.ts +256 -0
  54. package/src/protocol/errors.ts +5 -0
  55. package/src/protocol/index.ts +1 -0
  56. package/src/protocol/sse.ts +38 -3
  57. package/src/protocol/types.ts +7 -0
  58. package/src/protocol/version.ts +2 -2
  59. package/src/templates.ts +8 -4
@@ -13,11 +13,15 @@ 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.12.2' as const;
16
+ export const SHARED_PACKAGE_VERSION = '0.13.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. */
20
20
  export const ROLE_TEXT_MAX_BYTES = 51_200 as const;
21
+ export const DEFAULT_LOG_ENTRY_ADVISORY_BYTES = 1024 as const;
22
+ export const DEFAULT_MAX_LOG_ENTRY_BYTES = 4096 as const;
23
+ export const LOG_ENTRY_ADVISORY_ENV = 'BORG_SERVER_LOG_ENTRY_ADVISORY_BYTES' as const;
24
+ export const MAX_LOG_ENTRY_ENV = 'BORG_SERVER_MAX_LOG_ENTRY_BYTES' as const;
21
25
 
22
26
  export const HEALTH_PATH = '/healthz' as const;
23
27
  export const PROTOCOL_INFO_PATH = '/api/protocol' as const;
@@ -30,12 +34,18 @@ export const REPOSITORY_CUBE_RESOLVE_PATH = '/api/repository-cubes/resolve' as c
30
34
  export const REPOSITORY_CUBE_ASSOCIATION_PATH = '/api/repository-cubes/association' as const;
31
35
  export const ATTACH_PATH = '/api/client/attach' as const;
32
36
  export const SELF_RUNTIME_METADATA_PATH = '/api/cubes/:cubeId/drones/self/metadata' as const;
37
+ export const DOCUMENTS_PATH = '/api/cubes/:cubeId/documents' as const;
38
+ export const DOCUMENT_PATH = '/api/cubes/:cubeId/documents/:documentId' as const;
33
39
 
34
40
  export const PROTOCOL_HTTP_CONTRACT = {
35
41
  health: { method: 'GET', path: HEALTH_PATH, authenticated: false, success_status: 204, bodyless: true },
36
42
  protocol: { method: 'GET', path: PROTOCOL_INFO_PATH, authenticated: false, success_status: 200 },
37
43
  enrollment: { method: 'POST', path: ENROLLMENT_EXCHANGE_PATH, authenticated: 'invitation', success_status: 201 },
38
44
  cubes: { method: 'POST', path: CUBES_PATH, authenticated: true, success_status: 201 },
45
+ document_put: { method: 'PUT', path: DOCUMENTS_PATH, authenticated: true, success_status: 201, mutation: true },
46
+ document_list: { method: 'GET', path: DOCUMENTS_PATH, authenticated: true, success_status: 200, mutation: false },
47
+ document_get: { method: 'GET', path: DOCUMENT_PATH, authenticated: true, success_status: 200, mutation: false },
48
+ document_remove: { method: 'DELETE', path: DOCUMENT_PATH, authenticated: true, success_status: 200, mutation: true },
39
49
  cube_delete: {
40
50
  method: 'DELETE',
41
51
  path: CUBE_PATH,
@@ -104,7 +114,7 @@ export const PROTOCOL_HTTP_CONTRACT = {
104
114
 
105
115
  export const PROTOCOL_LIMIT_CEILINGS = {
106
116
  max_request_bytes: 10 * 1024 * 1024,
107
- max_log_message_bytes: 1024 * 1024,
117
+ max_log_message_bytes: 65_536,
108
118
  max_read_page_size: 500,
109
119
  max_replay_page_size: 1000,
110
120
  } as const;
@@ -580,7 +590,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
580
590
  exactKeys(input, ['protocol_version'], ['protocol_version']);
581
591
  if (input.protocol_version !== PROTOCOL_VERSION) {
582
592
  throw new ProtocolContractError(
583
- 'This client requires protocol v9. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
593
+ 'This client requires protocol v10. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
584
594
  ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
585
595
  ['protocol_version'],
586
596
  );
@@ -997,12 +1007,12 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
997
1007
  const input = record(value);
998
1008
  exactKeys(
999
1009
  input,
1000
- ['post_id', 'message', 'visibility', 'recipientDroneIds', 'class', 'to'],
1010
+ ['post_id', 'message', 'visibility', 'recipientDroneIds', 'class', 'to', 'documents'],
1001
1011
  ['post_id', 'message'],
1002
1012
  );
1003
1013
  const output: import('./types.js').AppendLogRequest = {
1004
1014
  post_id: decodeUuid(input.post_id, ['post_id']),
1005
- message: boundedString(input.message, 1, 10_240, ['message']),
1015
+ message: boundedString(input.message, 1, PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes, ['message']),
1006
1016
  };
1007
1017
  if (input.visibility !== undefined) {
1008
1018
  if (input.visibility !== 'broadcast' && input.visibility !== 'direct') {
@@ -1024,6 +1034,10 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
1024
1034
  if (input.to !== undefined) {
1025
1035
  output.to = decodeStringArray(input.to, 'to', 100, 120);
1026
1036
  }
1037
+ if (input.documents !== undefined) {
1038
+ output.documents = decodeStringArray(input.documents, 'documents', 100, 128)
1039
+ .map((id, index) => decodeOpaqueIdentifier(id, ['documents', index]));
1040
+ }
1027
1041
  return output;
1028
1042
  }
1029
1043
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ PROTOCOL_LIMIT_CEILINGS,
2
3
  ProtocolContractError,
3
4
  compareLogCursor,
4
5
  decodeCanonicalTimestamp,
@@ -376,7 +377,7 @@ function decodeUnreachableRecipient(
376
377
 
377
378
  export function decodeAppendLogResult(value: unknown): AppendLogResult {
378
379
  const input = object(value);
379
- exact(input, ['entry', 'deduplicated', 'routing', 'unreachableRecipients'], ['entry', 'deduplicated']);
380
+ exact(input, ['entry', 'deduplicated', 'routing', 'unreachableRecipients', 'advisory'], ['entry', 'deduplicated']);
380
381
  if (typeof input.deduplicated !== 'boolean') {
381
382
  throw new ProtocolContractError('Invalid append-log deduplicated flag.');
382
383
  }
@@ -393,6 +394,19 @@ export function decodeAppendLogResult(value: unknown): AppendLogResult {
393
394
  }
394
395
  output.unreachableRecipients = input.unreachableRecipients.map(decodeUnreachableRecipient);
395
396
  }
397
+ if (input.advisory !== undefined) {
398
+ const advisory = object(input.advisory);
399
+ exact(advisory, ['code', 'threshold_bytes'], ['code', 'threshold_bytes']);
400
+ if (advisory.code !== 'STORE_AS_DOCUMENT') {
401
+ throw new ProtocolContractError('Invalid append-log document advisory.');
402
+ }
403
+ const threshold = positiveInteger(
404
+ advisory.threshold_bytes,
405
+ 'advisory.threshold_bytes',
406
+ PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes,
407
+ );
408
+ output.advisory = { code: 'STORE_AS_DOCUMENT', threshold_bytes: threshold };
409
+ }
396
410
  return output;
397
411
  }
398
412
 
@@ -0,0 +1,256 @@
1
+ import { ErrorCode } from './errors.js';
2
+ import {
3
+ ProtocolContractError,
4
+ decodeCanonicalTimestamp,
5
+ decodeOpaqueIdentifier,
6
+ decodeProtocolEnvelope,
7
+ decodeUuid,
8
+ utf8ByteLength,
9
+ type ProtocolEnvelope,
10
+ } from './contract.js';
11
+
12
+ export const DOCUMENT_CONTENT_TYPES = ['text/markdown', 'text/plain'] as const;
13
+ export const DOCUMENT_DEFAULT_MAX_BYTES = 65_536 as const;
14
+ export const DOCUMENT_DEFAULT_MAX_ACTIVE_BYTES_PER_CUBE = 524_288 as const;
15
+ export const DOCUMENT_MAX_BYTES_ENV = 'BORG_SERVER_MAX_DOCUMENT_BYTES' as const;
16
+ export const DOCUMENT_MAX_ACTIVE_BYTES_PER_CUBE_ENV =
17
+ 'BORG_SERVER_MAX_ACTIVE_DOCUMENT_BYTES_PER_CUBE' as const;
18
+ export type DocumentContentType = (typeof DOCUMENT_CONTENT_TYPES)[number];
19
+ export type DocumentState = 'active' | 'superseded' | 'removed';
20
+
21
+ export interface DocumentActor {
22
+ drone_id: string | null;
23
+ label: string | null;
24
+ role: string | null;
25
+ }
26
+
27
+ export interface DocumentCitation {
28
+ id: string;
29
+ title: string;
30
+ size_bytes: number;
31
+ state: DocumentState;
32
+ }
33
+
34
+ export interface CubeDocumentMetadata extends DocumentCitation {
35
+ content_type: DocumentContentType;
36
+ supersedes: string | null;
37
+ superseded_by: string | null;
38
+ author: DocumentActor;
39
+ created_at: string;
40
+ removed_by: DocumentActor | null;
41
+ removed_at: string | null;
42
+ }
43
+
44
+ export interface CubeDocument extends CubeDocumentMetadata {
45
+ content: string;
46
+ }
47
+
48
+ export interface PutDocumentRequest {
49
+ title: string;
50
+ content_type: DocumentContentType;
51
+ content: string;
52
+ supersedes?: string;
53
+ }
54
+ export interface PutDocumentResult { document: CubeDocument }
55
+ export interface GetDocumentRequest { id: string }
56
+ export interface GetDocumentResult { document: CubeDocument }
57
+ export type ListDocumentsRequest = Record<string, never>;
58
+ export interface ListDocumentsResult { documents: CubeDocumentMetadata[] }
59
+ export interface RemoveDocumentRequest { id: string }
60
+ export interface RemoveDocumentResult { document: CubeDocumentMetadata }
61
+
62
+ function object(value: unknown): Record<string, unknown> {
63
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
64
+ throw new ProtocolContractError('Expected a document object.');
65
+ }
66
+ return value as Record<string, unknown>;
67
+ }
68
+
69
+ function exact(value: Record<string, unknown>, allowed: readonly string[], required: readonly string[]): void {
70
+ for (const key of Object.keys(value)) {
71
+ if (!allowed.includes(key)) throw new ProtocolContractError('Unknown document field.');
72
+ }
73
+ for (const key of required) {
74
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
75
+ throw new ProtocolContractError(`Missing document field "${key}".`);
76
+ }
77
+ }
78
+ }
79
+
80
+ function text(value: unknown, field: string, maximumBytes: number, allowEmpty = false): string {
81
+ if (typeof value !== 'string' || (!allowEmpty && value.length === 0) || utf8ByteLength(value) > maximumBytes) {
82
+ throw new ProtocolContractError(`Invalid document field "${field}".`);
83
+ }
84
+ for (let index = 0; index < value.length; index++) {
85
+ const code = value.charCodeAt(index);
86
+ if (code >= 0xd800 && code <= 0xdbff) {
87
+ const next = value.charCodeAt(index + 1);
88
+ if (!(next >= 0xdc00 && next <= 0xdfff)) throw new ProtocolContractError(`Invalid UTF-8 document field "${field}".`);
89
+ index++;
90
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
91
+ throw new ProtocolContractError(`Invalid UTF-8 document field "${field}".`);
92
+ }
93
+ }
94
+ return value;
95
+ }
96
+
97
+ function title(value: unknown): string {
98
+ const decoded = text(value, 'title', 480);
99
+ if (Array.from(decoded).length > 120 || decoded !== decoded.trim() || /[\u0000-\u001f\u007f-\u009f]/.test(decoded)) {
100
+ throw new ProtocolContractError('Invalid document field "title".');
101
+ }
102
+ return decoded;
103
+ }
104
+
105
+ function contentType(value: unknown): DocumentContentType {
106
+ if (!DOCUMENT_CONTENT_TYPES.includes(value as DocumentContentType)) {
107
+ throw new ProtocolContractError(
108
+ 'Unsupported document content type.',
109
+ ErrorCode.DOCUMENT_CONTENT_TYPE_UNSUPPORTED,
110
+ ['content_type'],
111
+ );
112
+ }
113
+ return value as DocumentContentType;
114
+ }
115
+
116
+ function count(value: unknown, field: string): number {
117
+ if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > 10 * 1024 * 1024) {
118
+ throw new ProtocolContractError(`Invalid document field "${field}".`);
119
+ }
120
+ return value as number;
121
+ }
122
+
123
+ function nullableId(value: unknown, field: string): string | null {
124
+ return value === null ? null : decodeOpaqueIdentifier(value, [field]);
125
+ }
126
+
127
+ function nullableText(value: unknown, field: string): string | null {
128
+ return value === null ? null : text(value, field, 120);
129
+ }
130
+
131
+ export function decodeDocumentActor(value: unknown): DocumentActor {
132
+ const input = object(value);
133
+ exact(input, ['drone_id', 'label', 'role'], ['drone_id', 'label', 'role']);
134
+ return {
135
+ drone_id: input.drone_id === null ? null : decodeUuid(input.drone_id, ['drone_id']),
136
+ label: nullableText(input.label, 'label'),
137
+ role: nullableText(input.role, 'role'),
138
+ };
139
+ }
140
+
141
+ export function decodeDocumentCitation(value: unknown): DocumentCitation {
142
+ const input = object(value);
143
+ exact(input, ['id', 'title', 'size_bytes', 'state'], ['id', 'title', 'size_bytes', 'state']);
144
+ if (!['active', 'superseded', 'removed'].includes(String(input.state))) {
145
+ throw new ProtocolContractError('Invalid document state.');
146
+ }
147
+ return {
148
+ id: decodeOpaqueIdentifier(input.id, ['id']),
149
+ title: title(input.title),
150
+ size_bytes: count(input.size_bytes, 'size_bytes'),
151
+ state: input.state as DocumentState,
152
+ };
153
+ }
154
+
155
+ export function decodeDocumentCitations(value: unknown): DocumentCitation[] {
156
+ if (!Array.isArray(value) || value.length < 1 || value.length > 100) {
157
+ throw new ProtocolContractError('Document citations must contain 1-100 entries.');
158
+ }
159
+ const citations = value.map(decodeDocumentCitation);
160
+ if (new Set(citations.map(({ id }) => id)).size !== citations.length) {
161
+ throw new ProtocolContractError('Document citation ids must be unique.');
162
+ }
163
+ return citations;
164
+ }
165
+
166
+ export function decodeCubeDocumentMetadata(value: unknown): CubeDocumentMetadata {
167
+ const input = object(value);
168
+ exact(input, ['id', 'title', 'size_bytes', 'state', 'content_type', 'supersedes', 'superseded_by', 'author', 'created_at', 'removed_by', 'removed_at'], ['id', 'title', 'size_bytes', 'state', 'content_type', 'supersedes', 'superseded_by', 'author', 'created_at', 'removed_by', 'removed_at']);
169
+ const citation = decodeDocumentCitation({ id: input.id, title: input.title, size_bytes: input.size_bytes, state: input.state });
170
+ const removed = citation.state === 'removed';
171
+ const hasRemovedBy = input.removed_by !== null;
172
+ const hasRemovedAt = input.removed_at !== null;
173
+ if (hasRemovedBy !== hasRemovedAt || removed !== hasRemovedBy) {
174
+ throw new ProtocolContractError('Removed document audit fields do not match its state.');
175
+ }
176
+ if (citation.state === 'active' && input.superseded_by !== null) {
177
+ throw new ProtocolContractError('Active document cannot have a superseding revision.');
178
+ }
179
+ if (citation.state === 'superseded' && input.superseded_by === null) {
180
+ throw new ProtocolContractError('Superseded document must identify its next revision.');
181
+ }
182
+ return {
183
+ ...citation,
184
+ content_type: contentType(input.content_type),
185
+ supersedes: nullableId(input.supersedes, 'supersedes'),
186
+ superseded_by: nullableId(input.superseded_by, 'superseded_by'),
187
+ author: decodeDocumentActor(input.author),
188
+ created_at: decodeCanonicalTimestamp(input.created_at, ['created_at']),
189
+ removed_by: input.removed_by === null ? null : decodeDocumentActor(input.removed_by),
190
+ removed_at: input.removed_at === null ? null : decodeCanonicalTimestamp(input.removed_at, ['removed_at']),
191
+ };
192
+ }
193
+
194
+ export function decodeCubeDocument(value: unknown): CubeDocument {
195
+ const input = object(value);
196
+ const content = text(input.content, 'content', 10 * 1024 * 1024, true);
197
+ const { content: _content, ...metadataInput } = input;
198
+ const metadata = decodeCubeDocumentMetadata(metadataInput);
199
+ if (metadata.size_bytes !== utf8ByteLength(content)) throw new ProtocolContractError('Document size does not match its UTF-8 content.');
200
+ return { ...metadata, content };
201
+ }
202
+
203
+ export function decodePutDocumentRequest(value: unknown): PutDocumentRequest {
204
+ const input = object(value);
205
+ exact(input, ['title', 'content_type', 'content', 'supersedes'], ['title', 'content_type', 'content']);
206
+ const output: PutDocumentRequest = {
207
+ title: title(input.title),
208
+ content_type: contentType(input.content_type),
209
+ content: text(input.content, 'content', 10 * 1024 * 1024, true),
210
+ };
211
+ if (input.supersedes !== undefined) output.supersedes = decodeOpaqueIdentifier(input.supersedes, ['supersedes']);
212
+ return output;
213
+ }
214
+
215
+ export function decodeGetDocumentRequest(value: unknown): GetDocumentRequest {
216
+ const input = object(value); exact(input, ['id'], ['id']);
217
+ return { id: decodeOpaqueIdentifier(input.id, ['id']) };
218
+ }
219
+ export function decodeListDocumentsRequest(value: unknown): ListDocumentsRequest {
220
+ const input = object(value); exact(input, [], []); return {};
221
+ }
222
+ export const decodeRemoveDocumentRequest = decodeGetDocumentRequest;
223
+
224
+ function oneDocument<T>(value: unknown, decode: (input: unknown) => T): { document: T } {
225
+ const input = object(value); exact(input, ['document'], ['document']);
226
+ return { document: decode(input.document) };
227
+ }
228
+ export const decodePutDocumentResult = (value: unknown): PutDocumentResult => {
229
+ const result = oneDocument(value, decodeCubeDocument);
230
+ if (result.document.state !== 'active' || result.document.removed_at !== null || result.document.removed_by !== null) {
231
+ throw new ProtocolContractError('New document result must be active.');
232
+ }
233
+ return result;
234
+ };
235
+ export const decodeGetDocumentResult = (value: unknown): GetDocumentResult => oneDocument(value, decodeCubeDocument);
236
+ export const decodeRemoveDocumentResult = (value: unknown): RemoveDocumentResult => {
237
+ const result = oneDocument(value, decodeCubeDocumentMetadata);
238
+ if (result.document.state !== 'removed') throw new ProtocolContractError('Removed document result must be removed.');
239
+ return result;
240
+ };
241
+ export function decodeListDocumentsResult(value: unknown): ListDocumentsResult {
242
+ const input = object(value); exact(input, ['documents'], ['documents']);
243
+ if (!Array.isArray(input.documents) || input.documents.length > 500) throw new ProtocolContractError('Invalid document list.');
244
+ const documents = input.documents.map(decodeCubeDocumentMetadata);
245
+ if (documents.some(({ state }) => state === 'removed')) throw new ProtocolContractError('Removed documents must be delisted.');
246
+ return { documents };
247
+ }
248
+
249
+ export const decodePutDocumentRequestEnvelope = (value: unknown): ProtocolEnvelope<PutDocumentRequest> => decodeProtocolEnvelope(value, decodePutDocumentRequest);
250
+ export const decodePutDocumentResultEnvelope = (value: unknown): ProtocolEnvelope<PutDocumentResult> => decodeProtocolEnvelope(value, decodePutDocumentResult);
251
+ export const decodeGetDocumentRequestEnvelope = (value: unknown): ProtocolEnvelope<GetDocumentRequest> => decodeProtocolEnvelope(value, decodeGetDocumentRequest);
252
+ export const decodeGetDocumentResultEnvelope = (value: unknown): ProtocolEnvelope<GetDocumentResult> => decodeProtocolEnvelope(value, decodeGetDocumentResult);
253
+ export const decodeListDocumentsRequestEnvelope = (value: unknown): ProtocolEnvelope<ListDocumentsRequest> => decodeProtocolEnvelope(value, decodeListDocumentsRequest);
254
+ export const decodeListDocumentsResultEnvelope = (value: unknown): ProtocolEnvelope<ListDocumentsResult> => decodeProtocolEnvelope(value, decodeListDocumentsResult);
255
+ export const decodeRemoveDocumentRequestEnvelope = (value: unknown): ProtocolEnvelope<RemoveDocumentRequest> => decodeProtocolEnvelope(value, decodeRemoveDocumentRequest);
256
+ export const decodeRemoveDocumentResultEnvelope = (value: unknown): ProtocolEnvelope<RemoveDocumentResult> => decodeProtocolEnvelope(value, decodeRemoveDocumentResult);
@@ -22,6 +22,11 @@ export enum ErrorCode {
22
22
  ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
23
23
  ROLE_SECTION_NOT_FOUND = 'ROLE_SECTION_NOT_FOUND',
24
24
  ROLE_HAS_FROZEN_DRONES = 'ROLE_HAS_FROZEN_DRONES',
25
+ DOCUMENT_NOT_FOUND = 'DOCUMENT_NOT_FOUND',
26
+ DOCUMENT_CONTENT_TYPE_UNSUPPORTED = 'DOCUMENT_CONTENT_TYPE_UNSUPPORTED',
27
+ DOCUMENT_BUDGET_EXCEEDED = 'DOCUMENT_BUDGET_EXCEEDED',
28
+ DOCUMENT_SUPERSESSION_INVALID = 'DOCUMENT_SUPERSESSION_INVALID',
29
+ DOCUMENT_REMOVE_DENIED = 'DOCUMENT_REMOVE_DENIED',
25
30
  CUBE_DELETED = 'CUBE_DELETED',
26
31
  DRONE_EVICTED = 'DRONE_EVICTED',
27
32
  DRONE_FROZEN = 'DRONE_FROZEN',
@@ -3,5 +3,6 @@ export * from './types.js';
3
3
  export * from './version.js';
4
4
  export * from './contract.js';
5
5
  export * from './coordination.js';
6
+ export * from './documents.js';
6
7
  export * from './sse.js';
7
8
  export type { BroadcastHwm } from '../log-stream-hwm.js';
@@ -1,4 +1,5 @@
1
1
  import type { EnrichedStreamEntry } from './types.js';
2
+ import { decodeDocumentCitations } from './documents.js';
2
3
  import {
3
4
  ProtocolContractError,
4
5
  decodeCanonicalTimestamp,
@@ -6,15 +7,41 @@ import {
6
7
  decodeLogCursor,
7
8
  decodeOpaqueIdentifier,
8
9
  decodeUuid,
10
+ PROTOCOL_LIMIT_CEILINGS,
9
11
  utf8ByteLength,
10
12
  type LogCursor,
11
13
  type ProtocolErrorEnvelope,
12
14
  } from './contract.js';
13
15
 
16
+ const MAX_UUID = '00000000-0000-4000-8000-000000000000';
17
+ const MAX_TIMESTAMP = '0000-00-00T00:00:00.000Z';
18
+ const MAX_LOG_DATA_BYTES = utf8ByteLength(JSON.stringify({
19
+ cursor: { created_at: MAX_TIMESTAMP, id: MAX_UUID },
20
+ entry: {
21
+ id: MAX_UUID,
22
+ cube_id: MAX_UUID,
23
+ drone_id: MAX_UUID,
24
+ message: '\0'.repeat(PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes),
25
+ visibility: 'broadcast',
26
+ created_at: MAX_TIMESTAMP,
27
+ drone_label: '\0'.repeat(120),
28
+ role_name: '\0'.repeat(120),
29
+ recipient_drone_ids: Array.from({ length: 100 }, () => MAX_UUID),
30
+ documents: Array.from({ length: 100 }, (_, index) => ({
31
+ id: `${index.toString().padStart(3, '0')}${'x'.repeat(125)}`,
32
+ title: '😀'.repeat(120),
33
+ size_bytes: 10 * 1024 * 1024,
34
+ state: 'superseded',
35
+ })),
36
+ },
37
+ }));
38
+ const MAX_LOG_FRAME_BYTES = MAX_LOG_DATA_BYTES +
39
+ utf8ByteLength(`event: log\nid: ${MAX_UUID}\ndata: `);
40
+
14
41
  export const SSE_LIMITS = {
15
42
  total_bytes: 1024 * 1024,
16
- frame_bytes: 65_536,
17
- data_bytes: 65_536,
43
+ frame_bytes: MAX_LOG_FRAME_BYTES,
44
+ data_bytes: MAX_LOG_DATA_BYTES,
18
45
  frame_count: 1000,
19
46
  unknown_data_bytes: 4096,
20
47
  } as const;
@@ -97,6 +124,7 @@ export function decodeEnrichedStreamEntry(value: unknown): EnrichedStreamEntry {
97
124
  'drone_label',
98
125
  'role_name',
99
126
  'recipient_drone_ids',
127
+ 'documents',
100
128
  ],
101
129
  [
102
130
  'id',
@@ -120,7 +148,11 @@ export function decodeEnrichedStreamEntry(value: unknown): EnrichedStreamEntry {
120
148
  id: decodeUuid(entry.id, ['entry', 'id']),
121
149
  cube_id: decodeUuid(entry.cube_id, ['entry', 'cube_id']),
122
150
  drone_id: entry.drone_id === null ? null : decodeUuid(entry.drone_id, ['entry', 'drone_id']),
123
- message: boundedString(entry.message, 'message', 10_240),
151
+ message: boundedString(
152
+ entry.message,
153
+ 'message',
154
+ PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes,
155
+ ),
124
156
  visibility: entry.visibility,
125
157
  created_at: decodeCanonicalTimestamp(entry.created_at, ['entry', 'created_at']),
126
158
  drone_label: nullableString(entry.drone_label, 'drone_label', 120),
@@ -128,6 +160,9 @@ export function decodeEnrichedStreamEntry(value: unknown): EnrichedStreamEntry {
128
160
  recipient_drone_ids: entry.recipient_drone_ids.map((id, index) =>
129
161
  decodeUuid(id, ['entry', 'recipient_drone_ids', index])
130
162
  ),
163
+ ...(entry.documents === undefined ? {} : {
164
+ documents: decodeDocumentCitations(entry.documents),
165
+ }),
131
166
  };
132
167
  }
133
168
 
@@ -1,4 +1,5 @@
1
1
  import type { MessageTaxonomy } from '../templates.js';
2
+ import type { DocumentCitation } from './documents.js';
2
3
 
3
4
  export type AgentKind = 'claude' | 'codex' | 'opencode';
4
5
  export type RoleClass = 'queen' | 'worker';
@@ -100,6 +101,7 @@ export interface ActivityLogEntry {
100
101
  message: string;
101
102
  visibility: LogVisibility;
102
103
  created_at: string;
104
+ documents?: DocumentCitation[];
103
105
  }
104
106
 
105
107
  export interface EnrichedStreamEntry extends ActivityLogEntry {
@@ -177,6 +179,7 @@ export interface AppendLogRequest {
177
179
  recipientDroneIds?: string[];
178
180
  class?: string;
179
181
  to?: string[];
182
+ documents?: string[];
180
183
  }
181
184
 
182
185
  export interface AppendLogResponse {
@@ -184,6 +187,10 @@ export interface AppendLogResponse {
184
187
  deduplicated: boolean;
185
188
  routing?: RoutingEcho | null;
186
189
  unreachableRecipients?: Array<{ id: string; label: string }>;
190
+ advisory?: {
191
+ code: 'STORE_AS_DOCUMENT';
192
+ threshold_bytes: number;
193
+ };
187
194
  }
188
195
 
189
196
  export interface Decision {
@@ -1,4 +1,4 @@
1
- /** Current Borg coordination protocol generation. Clean-slate v9. */
2
- export const PROTOCOL_VERSION = '9' as const;
1
+ /** Current Borg coordination protocol generation. Clean-slate v10. */
2
+ export const PROTOCOL_VERSION = '10' as const;
3
3
 
4
4
  export type ProtocolVersion = typeof PROTOCOL_VERSION;
package/src/templates.ts CHANGED
@@ -103,7 +103,8 @@ Integration and release:
103
103
  - Review approval does not itself authorize merge, deployment, publication, tagging, or release.
104
104
  - Perform those actions only when the user request or a standing delegation explicitly includes them.
105
105
  - Use the repository's protected workflow and bind every gate to the exact revision being integrated.
106
- - Never substitute, move, overwrite, or rerun an immutable release artifact without explicit recovery authority.`;
106
+ - A release tag starts the tag-restricted staging workflow automatically; npm stage approval is the sole human publication boundary.
107
+ - Before npm accepts a stage, correct a failed workflow and retry the same immutable tag. Never move, replace, or force-update the tag.`;
107
108
 
108
109
  export const GIT_OPERATIONAL_DISCIPLINE_BUILDER = `
109
110
 
@@ -200,7 +201,7 @@ const SOFTWARE_DEV_DIRECTIVE = `## Scope and coordination
200
201
  - When an outcome includes a separately published external surface, the Coordinator names one owning role or seat for its implementation. Other seats report findings or perform routed review; they do not mutate that surface.
201
202
  - Waiting is valid when no authorized action is available.
202
203
  - Merge, deploy, publish, tag, release, credential, and live-operator actions require explicit authority.
203
- - Keep cube-log signals concise. Put durable reasoning in the relevant issue, change, or existing maintained documentation only when it has an operational consumer.${SAME_REPOSITORY_WORKFLOW_DISCIPLINE}`;
204
+ - Keep cube-log signals concise. Put durable reasoning in the relevant issue, change, or existing maintained documentation only when it has an operational consumer.${SAME_REPOSITORY_WORKFLOW_DISCIPLINE}${RELEASE_CYCLE_SHAPES}`;
204
205
 
205
206
  const SOFTWARE_DEV_TAXONOMY: MessageTaxonomy = [
206
207
  {
@@ -308,6 +309,8 @@ Review:
308
309
  - Classify findings as in-scope blocker, touched-surface safety blocker, or out-of-scope finding.
309
310
  - Drop an observation when it changes no decision; do not create work merely to preserve it.
310
311
  - Reviewers provide evidence; they do not redefine the work unit. Route one due gate at a time, proportionate to the changed surface. Never pre-route a later gate.
312
+ - Require one proof per property. Mechanical, version, lock, and generated changes require exact-revision CI plus one Code Review only.
313
+ - Give a successor revision delta review. Carry unchanged green evidence without rerunning it.
311
314
  - Bind every verdict to the exact revision. Before claiming gate completion, reread the source log and verify every required verdict.
312
315
  - After two blocked rounds, stop and ask the human for the smallest next choice.
313
316
 
@@ -340,10 +343,11 @@ Implementation discipline:
340
343
  - Mark a deliberate corner-cut with a comment naming the known ceiling and the upgrade path.
341
344
 
342
345
  While working:
343
- - Post STARTING with the branch and first concrete action, then substantive PROGRESS during active work.
346
+ - Post STARTING with the branch and first concrete action. Omit PROGRESS for work expected to finish within 10 minutes; otherwise post only substantive PROGRESS during active work.
344
347
  - Do not add cleanup, broad refactors, speculative hardening, documentation programs, or follow-up issues unless assigned.
345
348
  - A discovered issue outside the slice is a finding, not permission to fix it.
346
- - Add proportionate tests for behavior you change. Run the repository checks required by the touched surface.
349
+ - Add proportionate tests for behavior you change. Run focused verification required by the touched surface, and do not rerun green CI checks merely to duplicate exact-revision evidence.
350
+ - Check documentation or a separately published site only when the changed behavior, public API, package metadata, or named user claim belongs to that surface.
347
351
 
348
352
  Handoff:
349
353
  - Verify the final diff contains only the authorized slice.