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.
- package/README.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js +3 -2
- package/dist/codex-launch.js.map +1 -1
- package/dist/docs-sections.d.ts.map +1 -1
- package/dist/docs-sections.js +8 -0
- package/dist/docs-sections.js.map +1 -1
- package/dist/document-render.d.ts +6 -0
- package/dist/document-render.d.ts.map +1 -0
- package/dist/document-render.js +34 -0
- package/dist/document-render.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +12 -0
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/launch-all-deps.d.ts +2 -0
- package/dist/launch-all-deps.d.ts.map +1 -1
- package/dist/launch-all-deps.js +13 -0
- package/dist/launch-all-deps.js.map +1 -1
- package/dist/log-stream.d.ts +2 -1
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +16 -3
- package/dist/log-stream.js.map +1 -1
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +6 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/remote-client.d.ts +6 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +41 -7
- package/dist/remote-client.js.map +1 -1
- package/dist/server-handshake.d.ts +4 -3
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js.map +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +65 -2
- package/dist/tool-manifest.js.map +1 -1
- package/dist/update-cmd.d.ts +5 -1
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +54 -14
- package/dist/update-cmd.js.map +1 -1
- package/docs/DOCUMENTS.md +49 -0
- package/docs/RELEASING.md +2 -2
- package/package.json +2 -2
- package/src/codex-launch.ts +3 -2
- package/src/docs-sections.ts +8 -0
- package/src/document-render.ts +41 -0
- package/src/index.ts +44 -1
- package/src/launch-all-cmd.ts +14 -0
- package/src/launch-all-deps.ts +15 -0
- package/src/log-stream.ts +21 -3
- package/src/regen-format.ts +6 -1
- package/src/remote-client.ts +108 -15
- package/src/server-handshake.ts +4 -3
- package/src/tool-manifest.ts +69 -2
- 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
|
|
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
|
|
package/src/launch-all-cmd.ts
CHANGED
|
@@ -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
|
package/src/launch-all-deps.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/regen-format.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/remote-client.ts
CHANGED
|
@@ -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
|
|
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');
|
package/src/server-handshake.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
221
|
-
* interrupted/lost response is recovered by re-sending the
|
|
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
|
*/
|
package/src/tool-manifest.ts
CHANGED
|
@@ -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
|
|
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
|
},
|