borgmcp 2.0.8 → 2.0.10
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 -2
- package/dist/agent-runtime.d.ts +2 -0
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +5 -1
- package/dist/agent-runtime.js.map +1 -1
- package/dist/assimilate-cmd.d.ts +2 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +2 -0
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +6 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -23
- package/dist/index.js.map +1 -1
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +6 -4
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +2 -0
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts +24 -9
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +329 -49
- package/dist/remote-client.js.map +1 -1
- package/dist/roster-render.d.ts +11 -0
- package/dist/roster-render.d.ts.map +1 -1
- package/dist/roster-render.js +41 -8
- package/dist/roster-render.js.map +1 -1
- package/dist/runtime-metadata.d.ts +13 -0
- package/dist/runtime-metadata.d.ts.map +1 -0
- package/dist/runtime-metadata.js +52 -0
- package/dist/runtime-metadata.js.map +1 -0
- package/dist/server-handshake.d.ts +2 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +3 -0
- package/dist/server-handshake.js.map +1 -1
- package/dist/sync-roles-render.d.ts +2 -0
- package/dist/sync-roles-render.d.ts.map +1 -1
- package/dist/sync-roles-render.js +26 -7
- package/dist/sync-roles-render.js.map +1 -1
- package/dist/working-repo.d.ts +5 -7
- package/dist/working-repo.d.ts.map +1 -1
- package/dist/working-repo.js +30 -40
- package/dist/working-repo.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +8 -7
- package/docs/LOCAL_SERVER.md +1 -1
- package/docs/RELEASING.md +19 -1
- package/package.json +2 -2
- package/src/agent-runtime.ts +8 -1
- package/src/assimilate-cmd.ts +3 -1
- package/src/assimilate-deps.ts +10 -4
- package/src/index.ts +58 -27
- package/src/regen-format.ts +12 -5
- package/src/regen.ts +2 -0
- package/src/remote-client.ts +382 -43
- package/src/roster-render.ts +58 -8
- package/src/runtime-metadata.ts +67 -0
- package/src/server-handshake.ts +7 -2
- package/src/sync-roles-render.ts +24 -7
- package/src/working-repo.ts +30 -41
package/src/remote-client.ts
CHANGED
|
@@ -16,11 +16,14 @@ import {
|
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
17
|
import {
|
|
18
18
|
createProtocolEnvelope,
|
|
19
|
+
decodeDroneRuntimeMetadataState,
|
|
19
20
|
decodeEvictDroneResult,
|
|
20
21
|
decodeProtocolEnvelope,
|
|
21
22
|
decodeProtocolErrorEnvelope,
|
|
22
23
|
decodeReassignDroneResult,
|
|
24
|
+
decodeUpdateDroneRuntimeMetadataResponse,
|
|
23
25
|
ErrorCode,
|
|
26
|
+
type AgentKind,
|
|
24
27
|
type EvictDroneResult,
|
|
25
28
|
type ReassignDroneResult,
|
|
26
29
|
} from 'borgmcp-shared/protocol';
|
|
@@ -29,7 +32,11 @@ import { debugLog } from './debug.js';
|
|
|
29
32
|
import { assertUuidShape } from './evict-drone.js';
|
|
30
33
|
import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
|
|
31
34
|
import type { MessageTaxonomy, MessageTaxonomyClass } from 'borgmcp-shared/templates';
|
|
35
|
+
import { getTemplate, type Template, type TemplateRole } from 'borgmcp-shared/templates';
|
|
36
|
+
import { parseRoleSections } from 'borgmcp-shared/role-section';
|
|
37
|
+
import type { FragmentView, NonClobberSyncResult } from './sync-roles-render.js';
|
|
32
38
|
import type { WorkingRepo } from './working-repo.js';
|
|
39
|
+
import { buildRuntimeMetadataPatch } from './runtime-metadata.js';
|
|
33
40
|
import { loadBorgServerTrust, type ServerFetch } from './server-trust.js';
|
|
34
41
|
import {
|
|
35
42
|
BorgServerError,
|
|
@@ -250,6 +257,11 @@ export interface LocalManageOperation {
|
|
|
250
257
|
noMutation: string;
|
|
251
258
|
}
|
|
252
259
|
|
|
260
|
+
export interface LocalManageAuthority {
|
|
261
|
+
active: ActiveCube;
|
|
262
|
+
connection: RemoteConnection;
|
|
263
|
+
}
|
|
264
|
+
|
|
253
265
|
function manageCopyValue(value: string): string {
|
|
254
266
|
return JSON.stringify(value);
|
|
255
267
|
}
|
|
@@ -280,6 +292,13 @@ async function localManageConnection(
|
|
|
280
292
|
return { apiUrl: active.apiUrl, authToken, serverTrustIdentity: trustIdentity };
|
|
281
293
|
}
|
|
282
294
|
|
|
295
|
+
export async function resolveLocalManageAuthority(
|
|
296
|
+
active: ActiveCube,
|
|
297
|
+
operation: LocalManageOperation,
|
|
298
|
+
): Promise<LocalManageAuthority> {
|
|
299
|
+
return { active, connection: await localManageConnection(active, operation) };
|
|
300
|
+
}
|
|
301
|
+
|
|
283
302
|
async function localManageRequest<T>(
|
|
284
303
|
active: ActiveCube,
|
|
285
304
|
path: string,
|
|
@@ -287,8 +306,9 @@ async function localManageRequest<T>(
|
|
|
287
306
|
operation: LocalManageOperation,
|
|
288
307
|
payload?: Record<string, unknown>,
|
|
289
308
|
decodePayload?: (value: unknown) => T,
|
|
309
|
+
connectionOverride?: RemoteConnection,
|
|
290
310
|
): Promise<T | null> {
|
|
291
|
-
const connection = await localManageConnection(active, operation);
|
|
311
|
+
const connection = connectionOverride ?? await localManageConnection(active, operation);
|
|
292
312
|
try {
|
|
293
313
|
return await decodeLocalProtocolResponse<T>((signal) => authedFetch(path, {
|
|
294
314
|
method,
|
|
@@ -335,6 +355,24 @@ async function localConnectionRequest<T>(
|
|
|
335
355
|
}), false) as Promise<T>;
|
|
336
356
|
}
|
|
337
357
|
|
|
358
|
+
async function localConnectionMutation<T>(
|
|
359
|
+
connection: RemoteConnection,
|
|
360
|
+
path: string,
|
|
361
|
+
method: 'POST' | 'PATCH',
|
|
362
|
+
payload: Record<string, unknown>,
|
|
363
|
+
): Promise<T> {
|
|
364
|
+
return decodeLocalProtocolResponse<T>((signal) => authedFetch(path, {
|
|
365
|
+
method,
|
|
366
|
+
signal,
|
|
367
|
+
apiUrl: connection.apiUrl,
|
|
368
|
+
authToken: connection.authToken,
|
|
369
|
+
serverTrustIdentity: connection.serverTrustIdentity,
|
|
370
|
+
redirect: 'error',
|
|
371
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
372
|
+
body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
|
|
373
|
+
}), false) as Promise<T>;
|
|
374
|
+
}
|
|
375
|
+
|
|
338
376
|
async function localOwnerConnection(connection?: RemoteConnection): Promise<RemoteConnection> {
|
|
339
377
|
if (connection) return connection;
|
|
340
378
|
const active = await getActiveCube();
|
|
@@ -366,18 +404,42 @@ async function localCubeComposition(active: ActiveCube): Promise<{
|
|
|
366
404
|
if (!cubePayload || !rolePayload || !dronePayload) {
|
|
367
405
|
throw new Error('Local Borg server returned an incomplete cube response');
|
|
368
406
|
}
|
|
369
|
-
const
|
|
407
|
+
const drones = dronePayload.drones.map(withValidatedRuntimeMetadata);
|
|
408
|
+
const drone = drones.find((candidate) => candidate.id === active.droneId);
|
|
370
409
|
const role = rolePayload.roles.find((candidate) => candidate.id === drone?.role_id);
|
|
371
410
|
if (!drone || !role) throw new Error('Local Borg server no longer recognizes this drone seat');
|
|
372
411
|
return {
|
|
373
412
|
cube: cubePayload.cube,
|
|
374
413
|
roles: rolePayload.roles,
|
|
375
|
-
drones
|
|
414
|
+
drones,
|
|
376
415
|
role,
|
|
377
416
|
drone,
|
|
378
417
|
};
|
|
379
418
|
}
|
|
380
419
|
|
|
420
|
+
function withValidatedRuntimeMetadata<T extends Record<string, unknown>>(drone: T): T {
|
|
421
|
+
const state = decodeDroneRuntimeMetadataState(drone);
|
|
422
|
+
return {
|
|
423
|
+
...drone,
|
|
424
|
+
...state.runtime_metadata,
|
|
425
|
+
runtime_metadata_reported: state.runtime_metadata_reported,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function updateOwnRuntimeMetadata(
|
|
430
|
+
active: ActiveCube,
|
|
431
|
+
patch: ReturnType<typeof buildRuntimeMetadataPatch>,
|
|
432
|
+
): Promise<void> {
|
|
433
|
+
const payload = await localServerRequest<Record<string, unknown>>(
|
|
434
|
+
active,
|
|
435
|
+
`/api/cubes/${active.cubeId}/drones/self/metadata`,
|
|
436
|
+
'PATCH',
|
|
437
|
+
{ ...patch },
|
|
438
|
+
);
|
|
439
|
+
if (!payload) throw new Error('Local Borg server returned an empty runtime metadata response');
|
|
440
|
+
decodeUpdateDroneRuntimeMetadataResponse(payload);
|
|
441
|
+
}
|
|
442
|
+
|
|
381
443
|
function localCursorBinding(active: ActiveCube) {
|
|
382
444
|
return {
|
|
383
445
|
origin: active.apiUrl,
|
|
@@ -706,7 +768,7 @@ export async function whoami(
|
|
|
706
768
|
sessionToken: string,
|
|
707
769
|
apiUrl: string,
|
|
708
770
|
serverTrustIdentity?: string,
|
|
709
|
-
): Promise<{ cube_id: string; cube_name: string; drone_id: string; drone_label: string; role_id: string; role_name: string }> {
|
|
771
|
+
): Promise<{ cube_id: string; cube_name: string; drone_id: string; drone_label: string; role_id: string; role_name: string; runtime_metadata: { agent_kind: AgentKind | null; reported_model: string | null; working_repo_name: string | null; working_repo_origin: string | null }; runtime_metadata_reported: boolean }> {
|
|
710
772
|
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
711
773
|
const composed = await localCubeComposition(local);
|
|
712
774
|
return {
|
|
@@ -716,6 +778,13 @@ export async function whoami(
|
|
|
716
778
|
drone_label: composed.drone.label,
|
|
717
779
|
role_id: composed.role.id,
|
|
718
780
|
role_name: composed.role.name,
|
|
781
|
+
runtime_metadata: {
|
|
782
|
+
agent_kind: composed.drone.agent_kind,
|
|
783
|
+
reported_model: composed.drone.reported_model,
|
|
784
|
+
working_repo_name: composed.drone.working_repo_name,
|
|
785
|
+
working_repo_origin: composed.drone.working_repo_origin,
|
|
786
|
+
},
|
|
787
|
+
runtime_metadata_reported: composed.drone.runtime_metadata_reported,
|
|
719
788
|
};
|
|
720
789
|
}
|
|
721
790
|
|
|
@@ -738,9 +807,32 @@ export async function getRoster(
|
|
|
738
807
|
serverTrustIdentity?: string,
|
|
739
808
|
): Promise<{ drones: any[]; roles: any[]; message_taxonomy?: MessageTaxonomy | null; since?: string | null }> {
|
|
740
809
|
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
741
|
-
if (since !== undefined)
|
|
810
|
+
if (since !== undefined) {
|
|
811
|
+
const [dronePayload, rolePayload, cubePayload] = await Promise.all([
|
|
812
|
+
localServerRequest<{ drones: any[]; since?: string | null }>(
|
|
813
|
+
local,
|
|
814
|
+
`/api/cubes/${local.cubeId}/drones?since=${encodeURIComponent(since)}`,
|
|
815
|
+
'GET',
|
|
816
|
+
),
|
|
817
|
+
localServerRequest<{ roles: any[] }>(local, `/api/cubes/${local.cubeId}/roles`, 'GET'),
|
|
818
|
+
localServerRequest<{ cube: any }>(local, `/api/cubes/${local.cubeId}`, 'GET'),
|
|
819
|
+
]);
|
|
820
|
+
if (!dronePayload || !rolePayload || !cubePayload) {
|
|
821
|
+
throw new Error('Local Borg server returned an incomplete roster response');
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
drones: dronePayload.drones.map(withValidatedRuntimeMetadata),
|
|
825
|
+
roles: rolePayload.roles,
|
|
826
|
+
message_taxonomy: cubePayload.cube.message_taxonomy ?? null,
|
|
827
|
+
since: dronePayload.since ?? since,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
742
830
|
const composed = await localCubeComposition(local);
|
|
743
|
-
return {
|
|
831
|
+
return {
|
|
832
|
+
drones: composed.drones,
|
|
833
|
+
roles: composed.roles,
|
|
834
|
+
message_taxonomy: composed.cube.message_taxonomy ?? null,
|
|
835
|
+
};
|
|
744
836
|
}
|
|
745
837
|
|
|
746
838
|
/**
|
|
@@ -887,6 +979,8 @@ export async function regen(
|
|
|
887
979
|
since?: string;
|
|
888
980
|
/** Advisory self-report from the running agent; never model-routing config. */
|
|
889
981
|
reportedModel?: string;
|
|
982
|
+
/** Positively identified running Agent CLI; null means explicitly unknown. */
|
|
983
|
+
agentKind?: AgentKind | null;
|
|
890
984
|
/** Current cwd-derived identity; refreshed each regen to avoid stale routing data. */
|
|
891
985
|
workingRepo?: WorkingRepo;
|
|
892
986
|
/** Verified self-hosted authority from the caller's first active-state read. */
|
|
@@ -913,6 +1007,24 @@ export async function regen(
|
|
|
913
1007
|
apiUrl,
|
|
914
1008
|
opts.serverTrustIdentity,
|
|
915
1009
|
);
|
|
1010
|
+
if (
|
|
1011
|
+
opts.agentKind !== undefined ||
|
|
1012
|
+
opts.reportedModel !== undefined ||
|
|
1013
|
+
opts.workingRepo !== undefined
|
|
1014
|
+
) {
|
|
1015
|
+
const patch = buildRuntimeMetadataPatch({
|
|
1016
|
+
agentKind: opts.agentKind ?? null,
|
|
1017
|
+
reportedModel: opts.reportedModel,
|
|
1018
|
+
workingRepo: opts.workingRepo,
|
|
1019
|
+
});
|
|
1020
|
+
try {
|
|
1021
|
+
await updateOwnRuntimeMetadata(local, patch);
|
|
1022
|
+
} catch {
|
|
1023
|
+
// Metadata is advisory. Preserve the prior server value and continue with
|
|
1024
|
+
// the authenticated identity read; never echo rejected local input.
|
|
1025
|
+
console.warn('Local regen: runtime metadata update unavailable; preserving the prior safe report.');
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
916
1028
|
const composed = await localCubeComposition(local);
|
|
917
1029
|
const cursor = opts.since === undefined
|
|
918
1030
|
? await getLocalServerCursor(localCursorBinding(local))
|
|
@@ -1071,11 +1183,25 @@ export async function createCube(
|
|
|
1071
1183
|
opts?: { template?: string; message_taxonomy?: MessageTaxonomy | null },
|
|
1072
1184
|
connection?: RemoteConnection,
|
|
1073
1185
|
): Promise<{ id: string; name: string; cube_directive?: string; roles: any[]; drones?: any[]; [k: string]: any }> {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1186
|
+
if (!name?.trim()) throw new Error('Local Borg server cube creation requires a cube name');
|
|
1187
|
+
if (opts?.template !== undefined && opts.template !== 'default') {
|
|
1188
|
+
throw new Error('Local Borg server supports only the default cube seed');
|
|
1189
|
+
}
|
|
1190
|
+
const resolved = await localOwnerConnection(connection);
|
|
1191
|
+
const created = await localConnectionMutation<{
|
|
1192
|
+
cube_id: string;
|
|
1193
|
+
human_seat_role_id: string;
|
|
1194
|
+
default_worker_role_id: string;
|
|
1195
|
+
}>(resolved, '/api/cubes', 'POST', {
|
|
1196
|
+
retry_key: randomUUID(),
|
|
1197
|
+
name: name.trim(),
|
|
1198
|
+
template: 'default',
|
|
1199
|
+
});
|
|
1200
|
+
if (!created?.cube_id) throw new Error('Local Borg server returned an invalid cube creation response');
|
|
1201
|
+
const patch: Record<string, unknown> = { cube_directive: cubeDirective };
|
|
1202
|
+
if (opts?.message_taxonomy !== undefined) patch.message_taxonomy = opts.message_taxonomy;
|
|
1203
|
+
await localConnectionMutation(resolved, `/api/cubes/${created.cube_id}`, 'PATCH', patch);
|
|
1204
|
+
return getCube(created.cube_id, resolved);
|
|
1079
1205
|
}
|
|
1080
1206
|
|
|
1081
1207
|
/**
|
|
@@ -1084,10 +1210,12 @@ export async function createCube(
|
|
|
1084
1210
|
*/
|
|
1085
1211
|
export async function updateCube(
|
|
1086
1212
|
cubeId: string,
|
|
1087
|
-
updates: { name?: string; cube_directive?: string; message_taxonomy?: MessageTaxonomy | null }
|
|
1213
|
+
updates: { name?: string; cube_directive?: string; message_taxonomy?: MessageTaxonomy | null },
|
|
1214
|
+
activeOverride?: ActiveCube,
|
|
1215
|
+
connectionOverride?: RemoteConnection,
|
|
1088
1216
|
): Promise<{ cube: any }> {
|
|
1089
1217
|
assertUuidShape(cubeId, 'cube_id');
|
|
1090
|
-
const active = await getActiveCube();
|
|
1218
|
+
const active = activeOverride ?? await getActiveCube();
|
|
1091
1219
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1092
1220
|
if (updates.name !== undefined) localUnsupported('cube rename');
|
|
1093
1221
|
const payload: Record<string, unknown> = {};
|
|
@@ -1105,6 +1233,8 @@ export async function updateCube(
|
|
|
1105
1233
|
noMutation: 'No cube settings were changed.',
|
|
1106
1234
|
},
|
|
1107
1235
|
payload,
|
|
1236
|
+
undefined,
|
|
1237
|
+
connectionOverride,
|
|
1108
1238
|
);
|
|
1109
1239
|
if (!result) throw new Error('Local Borg server returned an empty cube response');
|
|
1110
1240
|
return result;
|
|
@@ -1120,11 +1250,13 @@ export async function patchTaxonomyClass(
|
|
|
1120
1250
|
cubeId: string,
|
|
1121
1251
|
op:
|
|
1122
1252
|
| { action: 'add'; class_def: MessageTaxonomyClass }
|
|
1123
|
-
|
|
1124
|
-
|
|
1253
|
+
| { action: 'replace'; class_def: MessageTaxonomyClass }
|
|
1254
|
+
| { action: 'remove'; class: string },
|
|
1255
|
+
activeOverride?: ActiveCube,
|
|
1256
|
+
connectionOverride?: RemoteConnection,
|
|
1125
1257
|
): Promise<{ cube: any }> {
|
|
1126
1258
|
assertUuidShape(cubeId, 'cube_id');
|
|
1127
|
-
const active = await getActiveCube();
|
|
1259
|
+
const active = activeOverride ?? await getActiveCube();
|
|
1128
1260
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1129
1261
|
const className = op.action === 'remove' ? op.class : op.class_def.class;
|
|
1130
1262
|
const preposition = op.action === 'add' ? 'to' : op.action === 'replace' ? 'in' : 'from';
|
|
@@ -1140,6 +1272,8 @@ export async function patchTaxonomyClass(
|
|
|
1140
1272
|
noMutation: `No message class was ${pastTense}.`,
|
|
1141
1273
|
},
|
|
1142
1274
|
op,
|
|
1275
|
+
undefined,
|
|
1276
|
+
connectionOverride,
|
|
1143
1277
|
);
|
|
1144
1278
|
if (!result) throw new Error('Local Borg server returned an empty taxonomy response');
|
|
1145
1279
|
return result;
|
|
@@ -1160,10 +1294,12 @@ export async function deleteCube(cubeId: string): Promise<void> {
|
|
|
1160
1294
|
*/
|
|
1161
1295
|
export async function createRole(
|
|
1162
1296
|
cubeId: string,
|
|
1163
|
-
data: { name: string; short_description: string; detailed_description: string; is_default?: boolean; is_mandatory?: boolean; is_human_seat?: boolean; can_broadcast?: boolean; receives_all_direct?: boolean; default_model?: string; role_class?: 'queen' | 'worker' }
|
|
1297
|
+
data: { name: string; short_description: string; detailed_description: string; is_default?: boolean; is_mandatory?: boolean; is_human_seat?: boolean; can_broadcast?: boolean; receives_all_direct?: boolean; default_model?: string; role_class?: 'queen' | 'worker' },
|
|
1298
|
+
activeOverride?: ActiveCube,
|
|
1299
|
+
connectionOverride?: RemoteConnection,
|
|
1164
1300
|
): Promise<{ role: any }> {
|
|
1165
1301
|
assertUuidShape(cubeId, 'cube_id');
|
|
1166
|
-
const active = await getActiveCube();
|
|
1302
|
+
const active = activeOverride ?? await getActiveCube();
|
|
1167
1303
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1168
1304
|
if (data.default_model !== undefined) localUnsupported('per-role default model');
|
|
1169
1305
|
const result = await localManageRequest<{ role: any }>(
|
|
@@ -1176,6 +1312,8 @@ export async function createRole(
|
|
|
1176
1312
|
noMutation: 'No role was created.',
|
|
1177
1313
|
},
|
|
1178
1314
|
buildLocalRoleFields(data),
|
|
1315
|
+
undefined,
|
|
1316
|
+
connectionOverride,
|
|
1179
1317
|
);
|
|
1180
1318
|
if (!result) throw new Error('Local Borg server returned an empty role response');
|
|
1181
1319
|
return result;
|
|
@@ -1186,23 +1324,29 @@ export async function createRole(
|
|
|
1186
1324
|
*/
|
|
1187
1325
|
export async function updateRole(
|
|
1188
1326
|
roleId: string,
|
|
1189
|
-
updates: { name?: string; short_description?: string; detailed_description?: string; is_default?: boolean; is_mandatory?: boolean; is_human_seat?: boolean; can_broadcast?: boolean; receives_all_direct?: boolean; default_model?: string; role_class?: 'queen' | 'worker' }
|
|
1327
|
+
updates: { name?: string; short_description?: string; detailed_description?: string; is_default?: boolean; is_mandatory?: boolean; is_human_seat?: boolean; can_broadcast?: boolean; receives_all_direct?: boolean; default_model?: string; role_class?: 'queen' | 'worker' },
|
|
1328
|
+
targetCubeId?: string,
|
|
1329
|
+
activeOverride?: ActiveCube,
|
|
1330
|
+
connectionOverride?: RemoteConnection,
|
|
1190
1331
|
): Promise<{ role: any }> {
|
|
1191
1332
|
assertUuidShape(roleId, 'role_id');
|
|
1192
|
-
const active = await getActiveCube();
|
|
1333
|
+
const active = activeOverride ?? await getActiveCube();
|
|
1193
1334
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1194
|
-
|
|
1335
|
+
const cubeId = targetCubeId ?? active.cubeId;
|
|
1336
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
1195
1337
|
if (updates.default_model !== undefined) localUnsupported('per-role default model');
|
|
1196
1338
|
const result = await localManageRequest<{ role: any }>(
|
|
1197
1339
|
active,
|
|
1198
|
-
`/api/cubes/${
|
|
1340
|
+
`/api/cubes/${cubeId}/roles/${roleId}`,
|
|
1199
1341
|
'PATCH',
|
|
1200
1342
|
{
|
|
1201
|
-
operation: `update role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
|
|
1202
|
-
cubeName: active.name,
|
|
1343
|
+
operation: `update role ${manageCopyValue(roleId)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
1344
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1203
1345
|
noMutation: 'No role was updated.',
|
|
1204
1346
|
},
|
|
1205
1347
|
buildLocalRoleFields(updates),
|
|
1348
|
+
undefined,
|
|
1349
|
+
connectionOverride,
|
|
1206
1350
|
);
|
|
1207
1351
|
if (!result) throw new Error('Local Borg server returned an empty role response');
|
|
1208
1352
|
return result;
|
|
@@ -1256,22 +1400,28 @@ export async function patchRoleSection(
|
|
|
1256
1400
|
op:
|
|
1257
1401
|
| { action: 'replace'; heading: string; body: string }
|
|
1258
1402
|
| { action: 'insert'; heading: string; body: string; after?: string | null }
|
|
1259
|
-
| { action: 'delete'; heading: string }
|
|
1403
|
+
| { action: 'delete'; heading: string },
|
|
1404
|
+
targetCubeId?: string,
|
|
1405
|
+
activeOverride?: ActiveCube,
|
|
1406
|
+
connectionOverride?: RemoteConnection,
|
|
1260
1407
|
): Promise<{ role: any }> {
|
|
1261
1408
|
assertUuidShape(roleId, 'role_id');
|
|
1262
|
-
const active = await getActiveCube();
|
|
1409
|
+
const active = activeOverride ?? await getActiveCube();
|
|
1263
1410
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1264
|
-
|
|
1411
|
+
const cubeId = targetCubeId ?? active.cubeId;
|
|
1412
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
1265
1413
|
const result = await localManageRequest<{ role: any }>(
|
|
1266
1414
|
active,
|
|
1267
|
-
`/api/cubes/${
|
|
1415
|
+
`/api/cubes/${cubeId}/roles/${roleId}/section-patch`,
|
|
1268
1416
|
'POST',
|
|
1269
1417
|
{
|
|
1270
|
-
operation: `${op.action} section ${manageCopyValue(op.heading)} ${op.action === 'delete' ? 'from' : 'in'} role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
|
|
1271
|
-
cubeName: active.name,
|
|
1418
|
+
operation: `${op.action} section ${manageCopyValue(op.heading)} ${op.action === 'delete' ? 'from' : 'in'} role ${manageCopyValue(roleId)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
1419
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1272
1420
|
noMutation: `No role section was ${op.action === 'insert' ? 'inserted' : op.action === 'replace' ? 'replaced' : 'deleted'}.`,
|
|
1273
1421
|
},
|
|
1274
1422
|
{ ...op },
|
|
1423
|
+
undefined,
|
|
1424
|
+
connectionOverride,
|
|
1275
1425
|
);
|
|
1276
1426
|
if (!result) throw new Error('Local Borg server returned an empty role response');
|
|
1277
1427
|
return result;
|
|
@@ -1374,11 +1524,12 @@ export async function getCubeForManagement(
|
|
|
1374
1524
|
cubeId: string,
|
|
1375
1525
|
operation: LocalManageOperation,
|
|
1376
1526
|
activeOverride?: ActiveCube,
|
|
1527
|
+
connectionOverride?: RemoteConnection,
|
|
1377
1528
|
): Promise<{ id: string; name: string; roles: any[]; drones: any[]; [k: string]: any }> {
|
|
1378
1529
|
assertUuidShape(cubeId, 'cube_id');
|
|
1379
1530
|
const active = activeOverride ?? await getActiveCube();
|
|
1380
1531
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1381
|
-
return getCube(cubeId, await localManageConnection(active, operation));
|
|
1532
|
+
return getCube(cubeId, connectionOverride ?? await localManageConnection(active, operation));
|
|
1382
1533
|
}
|
|
1383
1534
|
|
|
1384
1535
|
/**
|
|
@@ -1398,7 +1549,7 @@ export async function getCube(cubeId: string, connection?: RemoteConnection): Pr
|
|
|
1398
1549
|
return {
|
|
1399
1550
|
...cubePayload.cube,
|
|
1400
1551
|
roles: rolePayload.roles,
|
|
1401
|
-
drones: dronePayload.drones,
|
|
1552
|
+
drones: dronePayload.drones.map(withValidatedRuntimeMetadata),
|
|
1402
1553
|
};
|
|
1403
1554
|
}
|
|
1404
1555
|
|
|
@@ -1413,11 +1564,44 @@ export async function getCube(cubeId: string, connection?: RemoteConnection): Pr
|
|
|
1413
1564
|
*/
|
|
1414
1565
|
export async function applyTemplate(
|
|
1415
1566
|
cubeId: string,
|
|
1416
|
-
templateName: string
|
|
1567
|
+
templateName: string,
|
|
1568
|
+
authorityOverride?: LocalManageAuthority,
|
|
1417
1569
|
): Promise<{ created: number; updated: number }> {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1570
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
1571
|
+
const template = getTemplate(templateName);
|
|
1572
|
+
if (!template) throw new Error(`Unknown Borg template ${JSON.stringify(templateName)}`);
|
|
1573
|
+
const active = authorityOverride?.active ?? await getActiveCube();
|
|
1574
|
+
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1575
|
+
const authority = authorityOverride ?? await resolveLocalManageAuthority(active, {
|
|
1576
|
+
operation: `apply template ${manageCopyValue(templateName)}`,
|
|
1577
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1578
|
+
noMutation: 'No template fragments were changed.',
|
|
1579
|
+
});
|
|
1580
|
+
const current = await getCubeForManagement(cubeId, {
|
|
1581
|
+
operation: `apply template ${manageCopyValue(templateName)}`,
|
|
1582
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1583
|
+
noMutation: 'No template fragments were changed.',
|
|
1584
|
+
}, active, authority.connection);
|
|
1585
|
+
let created = 0;
|
|
1586
|
+
let updated = 0;
|
|
1587
|
+
for (const role of template.roles) {
|
|
1588
|
+
const existing = current.roles.find((candidate) => candidate.name === role.name);
|
|
1589
|
+
if (!existing) {
|
|
1590
|
+
await createRole(cubeId, role, active, authority.connection);
|
|
1591
|
+
created++;
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
if (await applyMissingRoleFields(existing, role, cubeId, active, authority.connection)) updated++;
|
|
1595
|
+
updated += await applyMissingRoleSections(existing, role, cubeId, active, authority.connection);
|
|
1596
|
+
}
|
|
1597
|
+
for (const classDef of template.message_taxonomy ?? []) {
|
|
1598
|
+
const currentClasses = (current.message_taxonomy ?? []) as MessageTaxonomy;
|
|
1599
|
+
if (!currentClasses.some((candidate) => candidate.class === classDef.class)) {
|
|
1600
|
+
await patchTaxonomyClass(cubeId, { action: 'add', class_def: classDef }, active, authority.connection);
|
|
1601
|
+
updated++;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return { created, updated };
|
|
1421
1605
|
}
|
|
1422
1606
|
|
|
1423
1607
|
/**
|
|
@@ -1435,11 +1619,166 @@ export async function syncRoles(
|
|
|
1435
1619
|
cubeId: string,
|
|
1436
1620
|
templateName: string = 'software-dev',
|
|
1437
1621
|
apply: boolean = false,
|
|
1438
|
-
decisions?: Record<string, 'accept' | 'reject'
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1622
|
+
decisions?: Record<string, 'accept' | 'reject'>,
|
|
1623
|
+
authorityOverride?: LocalManageAuthority,
|
|
1624
|
+
): Promise<NonClobberSyncResult> {
|
|
1625
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
1626
|
+
const template = getTemplate(templateName);
|
|
1627
|
+
if (!template) throw new Error(`Unknown Borg template ${JSON.stringify(templateName)}`);
|
|
1628
|
+
const active = authorityOverride?.active ?? await getActiveCube();
|
|
1629
|
+
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1630
|
+
const authority = authorityOverride ?? await resolveLocalManageAuthority(active, {
|
|
1631
|
+
operation: `sync template ${manageCopyValue(templateName)}`,
|
|
1632
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1633
|
+
noMutation: 'No role synchronization changes were applied.',
|
|
1634
|
+
});
|
|
1635
|
+
const current = await getCubeForManagement(cubeId, {
|
|
1636
|
+
operation: `sync template ${manageCopyValue(templateName)}`,
|
|
1637
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1638
|
+
noMutation: 'No role synchronization changes were applied.',
|
|
1639
|
+
}, active, authority.connection);
|
|
1640
|
+
const roles: NonClobberSyncResult['roles'] = [];
|
|
1641
|
+
const taxonomy: FragmentView[] = [];
|
|
1642
|
+
const additions: Array<{ key: string; run: () => Promise<void> }> = [];
|
|
1643
|
+
const conflictKeys = new Set<string>();
|
|
1644
|
+
for (const role of template.roles) {
|
|
1645
|
+
const existing = current.roles.find((candidate) => candidate.name === role.name);
|
|
1646
|
+
if (!existing) {
|
|
1647
|
+
const key = `role:${role.name}`;
|
|
1648
|
+
const fragments: FragmentView[] = [{
|
|
1649
|
+
key,
|
|
1650
|
+
kind: 'add',
|
|
1651
|
+
label: 'role',
|
|
1652
|
+
cubeValue: null,
|
|
1653
|
+
templateValue: role.name,
|
|
1654
|
+
}];
|
|
1655
|
+
roles.push({ name: role.name, status: 'new', fragments });
|
|
1656
|
+
additions.push({ key, run: async () => { await createRole(cubeId, role, active, authority.connection); } });
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
const fragments: FragmentView[] = [];
|
|
1660
|
+
addRoleScalarFragment(fragments, additions, conflictKeys, decisions, existing, role, 'short_description', 'short description', cubeId, active, authority.connection);
|
|
1661
|
+
for (const field of ['is_default', 'is_mandatory', 'is_human_seat', 'can_broadcast', 'receives_all_direct'] as const) {
|
|
1662
|
+
if (role[field] !== undefined) addRoleScalarFragment(fragments, additions, conflictKeys, decisions, existing, role, field, field, cubeId, active, authority.connection);
|
|
1663
|
+
}
|
|
1664
|
+
const currentSections = new Map(parseRoleSections(String(existing.detailed_description ?? '')).map((section) => [section.heading, section]));
|
|
1665
|
+
for (const section of parseRoleSections(role.detailed_description)) {
|
|
1666
|
+
if (!section.heading) continue;
|
|
1667
|
+
const key = `role:${role.name}:section:${section.heading}`;
|
|
1668
|
+
const previous = currentSections.get(section.heading);
|
|
1669
|
+
const kind = !previous ? 'add' : previous.body === section.body ? 'unchanged' : 'conflict';
|
|
1670
|
+
fragments.push({ key, kind, label: `section ${section.heading}`, cubeValue: previous?.body ?? null, templateValue: section.body });
|
|
1671
|
+
if (kind === 'add') additions.push({ key, run: async () => { await patchRoleSection(existing.id, { action: 'insert', heading: section.heading!, body: section.body }, cubeId, active, authority.connection); } });
|
|
1672
|
+
if (kind === 'conflict') conflictKeys.add(key);
|
|
1673
|
+
if (kind === 'conflict' && decisions?.[key] === 'accept') additions.push({ key, run: async () => { await patchRoleSection(existing.id, { action: 'replace', heading: section.heading!, body: section.body }, cubeId, active, authority.connection); } });
|
|
1674
|
+
}
|
|
1675
|
+
roles.push({ name: role.name, status: 'existing', fragments });
|
|
1676
|
+
}
|
|
1677
|
+
for (const existing of current.roles) {
|
|
1678
|
+
if (!template.roles.some((role) => role.name === existing.name)) {
|
|
1679
|
+
roles.push({ name: existing.name, status: 'custom-skipped', fragments: [] });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
for (const classDef of template.message_taxonomy ?? []) {
|
|
1683
|
+
const key = `taxonomy:${classDef.class}`;
|
|
1684
|
+
const currentClass = (current.message_taxonomy ?? []).find((candidate: MessageTaxonomyClass) => candidate.class === classDef.class);
|
|
1685
|
+
const currentValue = currentClass ? stableJson(currentClass) : null;
|
|
1686
|
+
const templateValue = stableJson(classDef);
|
|
1687
|
+
const kind = !currentClass ? 'add' : currentValue === templateValue ? 'unchanged' : 'conflict';
|
|
1688
|
+
taxonomy.push({ key, kind, label: `taxonomy class ${classDef.class}`, cubeValue: currentValue, templateValue });
|
|
1689
|
+
if (kind === 'add') additions.push({ key, run: async () => { await patchTaxonomyClass(cubeId, { action: 'add', class_def: classDef }, active, authority.connection); } });
|
|
1690
|
+
if (kind === 'conflict') conflictKeys.add(key);
|
|
1691
|
+
if (kind === 'conflict' && decisions?.[key] === 'accept') additions.push({ key, run: async () => { await patchTaxonomyClass(cubeId, { action: 'replace', class_def: classDef }, active, authority.connection); } });
|
|
1692
|
+
}
|
|
1693
|
+
const acceptedConflicts = [...conflictKeys].filter((key) => decisions?.[key] === 'accept');
|
|
1694
|
+
const rejectedConflicts = [...conflictKeys].filter((key) => decisions?.[key] !== 'accept');
|
|
1695
|
+
const classifiedKeys = new Set([...conflictKeys]);
|
|
1696
|
+
const unmatchedDecisions = Object.keys(decisions ?? {}).filter((key) => !classifiedKeys.has(key));
|
|
1697
|
+
const addedKeys = additions.filter(({ key }) => !conflictKeys.has(key)).map(({ key }) => key);
|
|
1698
|
+
if (apply) {
|
|
1699
|
+
for (const addition of additions) {
|
|
1700
|
+
if (conflictKeys.has(addition.key) && decisions?.[addition.key] !== 'accept') continue;
|
|
1701
|
+
await addition.run();
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return {
|
|
1705
|
+
dryRun: !apply,
|
|
1706
|
+
roles,
|
|
1707
|
+
taxonomy,
|
|
1708
|
+
applied: {
|
|
1709
|
+
added: apply ? addedKeys : [],
|
|
1710
|
+
acceptedConflicts: apply ? acceptedConflicts : [],
|
|
1711
|
+
},
|
|
1712
|
+
rejectedConflicts,
|
|
1713
|
+
unmatchedDecisions,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function stableJson(value: unknown): string {
|
|
1718
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
1719
|
+
if (value !== null && typeof value === 'object') {
|
|
1720
|
+
return `{${Object.entries(value as Record<string, unknown>)
|
|
1721
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1722
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
|
|
1723
|
+
.join(',')}}`;
|
|
1724
|
+
}
|
|
1725
|
+
return JSON.stringify(value);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function addRoleScalarFragment(
|
|
1729
|
+
fragments: FragmentView[],
|
|
1730
|
+
additions: Array<{ key: string; run: () => Promise<void> }>,
|
|
1731
|
+
conflictKeys: Set<string>,
|
|
1732
|
+
decisions: Record<string, 'accept' | 'reject'> | undefined,
|
|
1733
|
+
existing: any,
|
|
1734
|
+
template: TemplateRole,
|
|
1735
|
+
field: 'short_description' | 'is_default' | 'is_mandatory' | 'is_human_seat' | 'can_broadcast' | 'receives_all_direct',
|
|
1736
|
+
label: string,
|
|
1737
|
+
cubeId: string,
|
|
1738
|
+
active: ActiveCube,
|
|
1739
|
+
connection: RemoteConnection,
|
|
1740
|
+
): void {
|
|
1741
|
+
const templateValue = template[field];
|
|
1742
|
+
if (templateValue === undefined) return;
|
|
1743
|
+
const key = `role:${template.name}:${field}`;
|
|
1744
|
+
const currentValue = existing[field];
|
|
1745
|
+
const missing = currentValue === undefined || (field === 'short_description' && currentValue === '');
|
|
1746
|
+
const kind = missing ? 'add' : currentValue === templateValue ? 'unchanged' : 'conflict';
|
|
1747
|
+
fragments.push({ key, kind, label, cubeValue: missing ? null : String(currentValue), templateValue: String(templateValue) });
|
|
1748
|
+
if (kind === 'add') additions.push({
|
|
1749
|
+
key,
|
|
1750
|
+
run: async () => { await updateRole(existing.id, { [field]: templateValue } as Parameters<typeof updateRole>[1], cubeId, active, connection); },
|
|
1751
|
+
});
|
|
1752
|
+
if (kind === 'conflict') {
|
|
1753
|
+
conflictKeys.add(key);
|
|
1754
|
+
if (decisions?.[key] === 'accept') additions.push({
|
|
1755
|
+
key,
|
|
1756
|
+
run: async () => { await updateRole(existing.id, { [field]: templateValue } as Parameters<typeof updateRole>[1], cubeId, active, connection); },
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
async function applyMissingRoleFields(existing: any, template: TemplateRole, cubeId: string, active: ActiveCube, connection: RemoteConnection): Promise<boolean> {
|
|
1762
|
+
const updates: Record<string, unknown> = {};
|
|
1763
|
+
if ((existing.short_description === undefined || existing.short_description === '') && template.short_description) {
|
|
1764
|
+
updates.short_description = template.short_description;
|
|
1765
|
+
}
|
|
1766
|
+
for (const field of ['is_default', 'is_mandatory', 'is_human_seat', 'can_broadcast', 'receives_all_direct'] as const) {
|
|
1767
|
+
if (existing[field] === undefined && template[field] !== undefined) updates[field] = template[field];
|
|
1768
|
+
}
|
|
1769
|
+
if (Object.keys(updates).length === 0) return false;
|
|
1770
|
+
await updateRole(existing.id, updates as Parameters<typeof updateRole>[1], cubeId, active, connection);
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
async function applyMissingRoleSections(existing: any, template: TemplateRole, cubeId: string, active: ActiveCube, connection: RemoteConnection): Promise<number> {
|
|
1775
|
+
const currentSections = new Map(parseRoleSections(String(existing.detailed_description ?? '')).map((section) => [section.heading, section]));
|
|
1776
|
+
let updated = 0;
|
|
1777
|
+
for (const section of parseRoleSections(template.detailed_description)) {
|
|
1778
|
+
if (section.heading && !currentSections.has(section.heading)) {
|
|
1779
|
+
await patchRoleSection(existing.id, { action: 'insert', heading: section.heading, body: section.body }, cubeId, active, connection);
|
|
1780
|
+
updated++;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return updated;
|
|
1445
1784
|
}
|