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/dist/remote-client.js
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { getServerCredential, } from './config.js';
|
|
13
13
|
import { randomUUID } from 'node:crypto';
|
|
14
|
-
import { createProtocolEnvelope, decodeEvictDroneResult, decodeProtocolEnvelope, decodeProtocolErrorEnvelope, decodeReassignDroneResult, ErrorCode, } from 'borgmcp-shared/protocol';
|
|
14
|
+
import { createProtocolEnvelope, decodeDroneRuntimeMetadataState, decodeEvictDroneResult, decodeProtocolEnvelope, decodeProtocolErrorEnvelope, decodeReassignDroneResult, decodeUpdateDroneRuntimeMetadataResponse, ErrorCode, } from 'borgmcp-shared/protocol';
|
|
15
15
|
import { debugLog } from './debug.js';
|
|
16
16
|
import { assertUuidShape } from './evict-drone.js';
|
|
17
17
|
import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
|
|
18
|
+
import { getTemplate } from 'borgmcp-shared/templates';
|
|
19
|
+
import { parseRoleSections } from 'borgmcp-shared/role-section';
|
|
20
|
+
import { buildRuntimeMetadataPatch } from './runtime-metadata.js';
|
|
18
21
|
import { loadBorgServerTrust } from './server-trust.js';
|
|
19
22
|
import { BorgServerError, BorgServerHttpError, BorgServerTrustError, BorgServerUnreachableError, LocalManageCredentialUnavailableError, LocalManageRequiredError, } from './server-errors.js';
|
|
20
23
|
import { getActiveCube } from './cubes.js';
|
|
@@ -186,8 +189,11 @@ async function localManageConnection(active, operation) {
|
|
|
186
189
|
}
|
|
187
190
|
return { apiUrl: active.apiUrl, authToken, serverTrustIdentity: trustIdentity };
|
|
188
191
|
}
|
|
189
|
-
async function
|
|
190
|
-
|
|
192
|
+
export async function resolveLocalManageAuthority(active, operation) {
|
|
193
|
+
return { active, connection: await localManageConnection(active, operation) };
|
|
194
|
+
}
|
|
195
|
+
async function localManageRequest(active, path, method, operation, payload, decodePayload, connectionOverride) {
|
|
196
|
+
const connection = connectionOverride ?? await localManageConnection(active, operation);
|
|
191
197
|
try {
|
|
192
198
|
return await decodeLocalProtocolResponse((signal) => authedFetch(path, {
|
|
193
199
|
method,
|
|
@@ -224,6 +230,18 @@ async function localConnectionRequest(connection, path) {
|
|
|
224
230
|
headers: { Accept: 'application/json' },
|
|
225
231
|
}), false);
|
|
226
232
|
}
|
|
233
|
+
async function localConnectionMutation(connection, path, method, payload) {
|
|
234
|
+
return decodeLocalProtocolResponse((signal) => authedFetch(path, {
|
|
235
|
+
method,
|
|
236
|
+
signal,
|
|
237
|
+
apiUrl: connection.apiUrl,
|
|
238
|
+
authToken: connection.authToken,
|
|
239
|
+
serverTrustIdentity: connection.serverTrustIdentity,
|
|
240
|
+
redirect: 'error',
|
|
241
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
242
|
+
body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
|
|
243
|
+
}), false);
|
|
244
|
+
}
|
|
227
245
|
async function localOwnerConnection(connection) {
|
|
228
246
|
if (connection)
|
|
229
247
|
return connection;
|
|
@@ -250,18 +268,33 @@ async function localCubeComposition(active) {
|
|
|
250
268
|
if (!cubePayload || !rolePayload || !dronePayload) {
|
|
251
269
|
throw new Error('Local Borg server returned an incomplete cube response');
|
|
252
270
|
}
|
|
253
|
-
const
|
|
271
|
+
const drones = dronePayload.drones.map(withValidatedRuntimeMetadata);
|
|
272
|
+
const drone = drones.find((candidate) => candidate.id === active.droneId);
|
|
254
273
|
const role = rolePayload.roles.find((candidate) => candidate.id === drone?.role_id);
|
|
255
274
|
if (!drone || !role)
|
|
256
275
|
throw new Error('Local Borg server no longer recognizes this drone seat');
|
|
257
276
|
return {
|
|
258
277
|
cube: cubePayload.cube,
|
|
259
278
|
roles: rolePayload.roles,
|
|
260
|
-
drones
|
|
279
|
+
drones,
|
|
261
280
|
role,
|
|
262
281
|
drone,
|
|
263
282
|
};
|
|
264
283
|
}
|
|
284
|
+
function withValidatedRuntimeMetadata(drone) {
|
|
285
|
+
const state = decodeDroneRuntimeMetadataState(drone);
|
|
286
|
+
return {
|
|
287
|
+
...drone,
|
|
288
|
+
...state.runtime_metadata,
|
|
289
|
+
runtime_metadata_reported: state.runtime_metadata_reported,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async function updateOwnRuntimeMetadata(active, patch) {
|
|
293
|
+
const payload = await localServerRequest(active, `/api/cubes/${active.cubeId}/drones/self/metadata`, 'PATCH', { ...patch });
|
|
294
|
+
if (!payload)
|
|
295
|
+
throw new Error('Local Borg server returned an empty runtime metadata response');
|
|
296
|
+
decodeUpdateDroneRuntimeMetadataResponse(payload);
|
|
297
|
+
}
|
|
265
298
|
function localCursorBinding(active) {
|
|
266
299
|
return {
|
|
267
300
|
origin: active.apiUrl,
|
|
@@ -503,6 +536,13 @@ export async function whoami(sessionToken, apiUrl, serverTrustIdentity) {
|
|
|
503
536
|
drone_label: composed.drone.label,
|
|
504
537
|
role_id: composed.role.id,
|
|
505
538
|
role_name: composed.role.name,
|
|
539
|
+
runtime_metadata: {
|
|
540
|
+
agent_kind: composed.drone.agent_kind,
|
|
541
|
+
reported_model: composed.drone.reported_model,
|
|
542
|
+
working_repo_name: composed.drone.working_repo_name,
|
|
543
|
+
working_repo_origin: composed.drone.working_repo_origin,
|
|
544
|
+
},
|
|
545
|
+
runtime_metadata_reported: composed.drone.runtime_metadata_reported,
|
|
506
546
|
};
|
|
507
547
|
}
|
|
508
548
|
/**
|
|
@@ -519,10 +559,28 @@ export async function whoami(sessionToken, apiUrl, serverTrustIdentity) {
|
|
|
519
559
|
*/
|
|
520
560
|
export async function getRoster(sessionToken, apiUrl, since, serverTrustIdentity) {
|
|
521
561
|
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
522
|
-
if (since !== undefined)
|
|
523
|
-
|
|
562
|
+
if (since !== undefined) {
|
|
563
|
+
const [dronePayload, rolePayload, cubePayload] = await Promise.all([
|
|
564
|
+
localServerRequest(local, `/api/cubes/${local.cubeId}/drones?since=${encodeURIComponent(since)}`, 'GET'),
|
|
565
|
+
localServerRequest(local, `/api/cubes/${local.cubeId}/roles`, 'GET'),
|
|
566
|
+
localServerRequest(local, `/api/cubes/${local.cubeId}`, 'GET'),
|
|
567
|
+
]);
|
|
568
|
+
if (!dronePayload || !rolePayload || !cubePayload) {
|
|
569
|
+
throw new Error('Local Borg server returned an incomplete roster response');
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
drones: dronePayload.drones.map(withValidatedRuntimeMetadata),
|
|
573
|
+
roles: rolePayload.roles,
|
|
574
|
+
message_taxonomy: cubePayload.cube.message_taxonomy ?? null,
|
|
575
|
+
since: dronePayload.since ?? since,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
524
578
|
const composed = await localCubeComposition(local);
|
|
525
|
-
return {
|
|
579
|
+
return {
|
|
580
|
+
drones: composed.drones,
|
|
581
|
+
roles: composed.roles,
|
|
582
|
+
message_taxonomy: composed.cube.message_taxonomy ?? null,
|
|
583
|
+
};
|
|
526
584
|
}
|
|
527
585
|
/**
|
|
528
586
|
* Read recent log entries for the cube.
|
|
@@ -612,6 +670,23 @@ export async function removeDecision(sessionToken, apiUrl, selector, serverTrust
|
|
|
612
670
|
*/
|
|
613
671
|
export async function regen(sessionToken, apiUrl, opts = {}) {
|
|
614
672
|
const local = await localAuthorityContext(sessionToken, apiUrl, opts.serverTrustIdentity);
|
|
673
|
+
if (opts.agentKind !== undefined ||
|
|
674
|
+
opts.reportedModel !== undefined ||
|
|
675
|
+
opts.workingRepo !== undefined) {
|
|
676
|
+
const patch = buildRuntimeMetadataPatch({
|
|
677
|
+
agentKind: opts.agentKind ?? null,
|
|
678
|
+
reportedModel: opts.reportedModel,
|
|
679
|
+
workingRepo: opts.workingRepo,
|
|
680
|
+
});
|
|
681
|
+
try {
|
|
682
|
+
await updateOwnRuntimeMetadata(local, patch);
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// Metadata is advisory. Preserve the prior server value and continue with
|
|
686
|
+
// the authenticated identity read; never echo rejected local input.
|
|
687
|
+
console.warn('Local regen: runtime metadata update unavailable; preserving the prior safe report.');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
615
690
|
const composed = await localCubeComposition(local);
|
|
616
691
|
const cursor = opts.since === undefined
|
|
617
692
|
? await getLocalServerCursor(localCursorBinding(local))
|
|
@@ -707,19 +782,32 @@ export async function listCubes(connection) {
|
|
|
707
782
|
* Existing callers that read `body.cube` keep working (forward-compat).
|
|
708
783
|
*/
|
|
709
784
|
export async function createCube(name, cubeDirective, opts, connection) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
785
|
+
if (!name?.trim())
|
|
786
|
+
throw new Error('Local Borg server cube creation requires a cube name');
|
|
787
|
+
if (opts?.template !== undefined && opts.template !== 'default') {
|
|
788
|
+
throw new Error('Local Borg server supports only the default cube seed');
|
|
789
|
+
}
|
|
790
|
+
const resolved = await localOwnerConnection(connection);
|
|
791
|
+
const created = await localConnectionMutation(resolved, '/api/cubes', 'POST', {
|
|
792
|
+
retry_key: randomUUID(),
|
|
793
|
+
name: name.trim(),
|
|
794
|
+
template: 'default',
|
|
795
|
+
});
|
|
796
|
+
if (!created?.cube_id)
|
|
797
|
+
throw new Error('Local Borg server returned an invalid cube creation response');
|
|
798
|
+
const patch = { cube_directive: cubeDirective };
|
|
799
|
+
if (opts?.message_taxonomy !== undefined)
|
|
800
|
+
patch.message_taxonomy = opts.message_taxonomy;
|
|
801
|
+
await localConnectionMutation(resolved, `/api/cubes/${created.cube_id}`, 'PATCH', patch);
|
|
802
|
+
return getCube(created.cube_id, resolved);
|
|
715
803
|
}
|
|
716
804
|
/**
|
|
717
805
|
* Update a cube's name and/or cube_directive. Both fields are optional;
|
|
718
806
|
* pass only what changes.
|
|
719
807
|
*/
|
|
720
|
-
export async function updateCube(cubeId, updates) {
|
|
808
|
+
export async function updateCube(cubeId, updates, activeOverride, connectionOverride) {
|
|
721
809
|
assertUuidShape(cubeId, 'cube_id');
|
|
722
|
-
const active = await getActiveCube();
|
|
810
|
+
const active = activeOverride ?? await getActiveCube();
|
|
723
811
|
if (!active?.serverTrustIdentity)
|
|
724
812
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
725
813
|
if (updates.name !== undefined)
|
|
@@ -734,7 +822,7 @@ export async function updateCube(cubeId, updates) {
|
|
|
734
822
|
operation: `update cube settings in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
735
823
|
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
736
824
|
noMutation: 'No cube settings were changed.',
|
|
737
|
-
}, payload);
|
|
825
|
+
}, payload, undefined, connectionOverride);
|
|
738
826
|
if (!result)
|
|
739
827
|
throw new Error('Local Borg server returned an empty cube response');
|
|
740
828
|
return result;
|
|
@@ -745,9 +833,9 @@ export async function updateCube(cubeId, updates) {
|
|
|
745
833
|
* other classes unchanged. The server re-validates the full resulting
|
|
746
834
|
* array and requires the selected local client's live cube-manage grant.
|
|
747
835
|
*/
|
|
748
|
-
export async function patchTaxonomyClass(cubeId, op) {
|
|
836
|
+
export async function patchTaxonomyClass(cubeId, op, activeOverride, connectionOverride) {
|
|
749
837
|
assertUuidShape(cubeId, 'cube_id');
|
|
750
|
-
const active = await getActiveCube();
|
|
838
|
+
const active = activeOverride ?? await getActiveCube();
|
|
751
839
|
if (!active?.serverTrustIdentity)
|
|
752
840
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
753
841
|
const className = op.action === 'remove' ? op.class : op.class_def.class;
|
|
@@ -758,7 +846,7 @@ export async function patchTaxonomyClass(cubeId, op) {
|
|
|
758
846
|
operation: `${op.action} message class ${manageCopyValue(className)} ${preposition} cube ${manageCopyValue(cubeName)}`,
|
|
759
847
|
cubeName,
|
|
760
848
|
noMutation: `No message class was ${pastTense}.`,
|
|
761
|
-
}, op);
|
|
849
|
+
}, op, undefined, connectionOverride);
|
|
762
850
|
if (!result)
|
|
763
851
|
throw new Error('Local Borg server returned an empty taxonomy response');
|
|
764
852
|
return result;
|
|
@@ -775,9 +863,9 @@ export async function deleteCube(cubeId) {
|
|
|
775
863
|
* Create a role inside a cube. is_default=true demotes the previous
|
|
776
864
|
* default role; the cube always has exactly one default.
|
|
777
865
|
*/
|
|
778
|
-
export async function createRole(cubeId, data) {
|
|
866
|
+
export async function createRole(cubeId, data, activeOverride, connectionOverride) {
|
|
779
867
|
assertUuidShape(cubeId, 'cube_id');
|
|
780
|
-
const active = await getActiveCube();
|
|
868
|
+
const active = activeOverride ?? await getActiveCube();
|
|
781
869
|
if (!active?.serverTrustIdentity)
|
|
782
870
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
783
871
|
if (data.default_model !== undefined)
|
|
@@ -786,7 +874,7 @@ export async function createRole(cubeId, data) {
|
|
|
786
874
|
operation: `create role ${manageCopyValue(data.name)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
787
875
|
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
788
876
|
noMutation: 'No role was created.',
|
|
789
|
-
}, buildLocalRoleFields(data));
|
|
877
|
+
}, buildLocalRoleFields(data), undefined, connectionOverride);
|
|
790
878
|
if (!result)
|
|
791
879
|
throw new Error('Local Borg server returned an empty role response');
|
|
792
880
|
return result;
|
|
@@ -794,19 +882,20 @@ export async function createRole(cubeId, data) {
|
|
|
794
882
|
/**
|
|
795
883
|
* Update a role. All fields optional; pass only what changes.
|
|
796
884
|
*/
|
|
797
|
-
export async function updateRole(roleId, updates) {
|
|
885
|
+
export async function updateRole(roleId, updates, targetCubeId, activeOverride, connectionOverride) {
|
|
798
886
|
assertUuidShape(roleId, 'role_id');
|
|
799
|
-
const active = await getActiveCube();
|
|
887
|
+
const active = activeOverride ?? await getActiveCube();
|
|
800
888
|
if (!active?.serverTrustIdentity)
|
|
801
889
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
802
|
-
|
|
890
|
+
const cubeId = targetCubeId ?? active.cubeId;
|
|
891
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
803
892
|
if (updates.default_model !== undefined)
|
|
804
893
|
localUnsupported('per-role default model');
|
|
805
|
-
const result = await localManageRequest(active, `/api/cubes/${
|
|
806
|
-
operation: `update role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
|
|
807
|
-
cubeName: active.name,
|
|
894
|
+
const result = await localManageRequest(active, `/api/cubes/${cubeId}/roles/${roleId}`, 'PATCH', {
|
|
895
|
+
operation: `update role ${manageCopyValue(roleId)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
896
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
808
897
|
noMutation: 'No role was updated.',
|
|
809
|
-
}, buildLocalRoleFields(updates));
|
|
898
|
+
}, buildLocalRoleFields(updates), undefined, connectionOverride);
|
|
810
899
|
if (!result)
|
|
811
900
|
throw new Error('Local Borg server returned an empty role response');
|
|
812
901
|
return result;
|
|
@@ -842,17 +931,18 @@ function buildLocalRoleFields(fields) {
|
|
|
842
931
|
* client's live cube-manage grant. Sections are delimited by plain-label lines (e.g.
|
|
843
932
|
* `Workflow:`), NOT markdown headings.
|
|
844
933
|
*/
|
|
845
|
-
export async function patchRoleSection(roleId, op) {
|
|
934
|
+
export async function patchRoleSection(roleId, op, targetCubeId, activeOverride, connectionOverride) {
|
|
846
935
|
assertUuidShape(roleId, 'role_id');
|
|
847
|
-
const active = await getActiveCube();
|
|
936
|
+
const active = activeOverride ?? await getActiveCube();
|
|
848
937
|
if (!active?.serverTrustIdentity)
|
|
849
938
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
939
|
+
const cubeId = targetCubeId ?? active.cubeId;
|
|
940
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
941
|
+
const result = await localManageRequest(active, `/api/cubes/${cubeId}/roles/${roleId}/section-patch`, 'POST', {
|
|
942
|
+
operation: `${op.action} section ${manageCopyValue(op.heading)} ${op.action === 'delete' ? 'from' : 'in'} role ${manageCopyValue(roleId)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
943
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
854
944
|
noMutation: `No role section was ${op.action === 'insert' ? 'inserted' : op.action === 'replace' ? 'replaced' : 'deleted'}.`,
|
|
855
|
-
}, { ...op });
|
|
945
|
+
}, { ...op }, undefined, connectionOverride);
|
|
856
946
|
if (!result)
|
|
857
947
|
throw new Error('Local Borg server returned an empty role response');
|
|
858
948
|
return result;
|
|
@@ -920,12 +1010,12 @@ export async function listRoles(cubeId) {
|
|
|
920
1010
|
}
|
|
921
1011
|
return result.roles;
|
|
922
1012
|
}
|
|
923
|
-
export async function getCubeForManagement(cubeId, operation, activeOverride) {
|
|
1013
|
+
export async function getCubeForManagement(cubeId, operation, activeOverride, connectionOverride) {
|
|
924
1014
|
assertUuidShape(cubeId, 'cube_id');
|
|
925
1015
|
const active = activeOverride ?? await getActiveCube();
|
|
926
1016
|
if (!active?.serverTrustIdentity)
|
|
927
1017
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
928
|
-
return getCube(cubeId, await localManageConnection(active, operation));
|
|
1018
|
+
return getCube(cubeId, connectionOverride ?? await localManageConnection(active, operation));
|
|
929
1019
|
}
|
|
930
1020
|
/**
|
|
931
1021
|
* Fetch a cube's full detail: directive, roles (with detailed
|
|
@@ -944,7 +1034,7 @@ export async function getCube(cubeId, connection) {
|
|
|
944
1034
|
return {
|
|
945
1035
|
...cubePayload.cube,
|
|
946
1036
|
roles: rolePayload.roles,
|
|
947
|
-
drones: dronePayload.drones,
|
|
1037
|
+
drones: dronePayload.drones.map(withValidatedRuntimeMetadata),
|
|
948
1038
|
};
|
|
949
1039
|
}
|
|
950
1040
|
/**
|
|
@@ -956,10 +1046,45 @@ export async function getCube(cubeId, connection) {
|
|
|
956
1046
|
* `{ created, updated }` counts. To selectively take template versions of
|
|
957
1047
|
* conflicting fragments, use `syncRoles` with a `decisions` map instead.
|
|
958
1048
|
*/
|
|
959
|
-
export async function applyTemplate(cubeId, templateName) {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1049
|
+
export async function applyTemplate(cubeId, templateName, authorityOverride) {
|
|
1050
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
1051
|
+
const template = getTemplate(templateName);
|
|
1052
|
+
if (!template)
|
|
1053
|
+
throw new Error(`Unknown Borg template ${JSON.stringify(templateName)}`);
|
|
1054
|
+
const active = authorityOverride?.active ?? await getActiveCube();
|
|
1055
|
+
if (!active?.serverTrustIdentity)
|
|
1056
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1057
|
+
const authority = authorityOverride ?? await resolveLocalManageAuthority(active, {
|
|
1058
|
+
operation: `apply template ${manageCopyValue(templateName)}`,
|
|
1059
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1060
|
+
noMutation: 'No template fragments were changed.',
|
|
1061
|
+
});
|
|
1062
|
+
const current = await getCubeForManagement(cubeId, {
|
|
1063
|
+
operation: `apply template ${manageCopyValue(templateName)}`,
|
|
1064
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1065
|
+
noMutation: 'No template fragments were changed.',
|
|
1066
|
+
}, active, authority.connection);
|
|
1067
|
+
let created = 0;
|
|
1068
|
+
let updated = 0;
|
|
1069
|
+
for (const role of template.roles) {
|
|
1070
|
+
const existing = current.roles.find((candidate) => candidate.name === role.name);
|
|
1071
|
+
if (!existing) {
|
|
1072
|
+
await createRole(cubeId, role, active, authority.connection);
|
|
1073
|
+
created++;
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (await applyMissingRoleFields(existing, role, cubeId, active, authority.connection))
|
|
1077
|
+
updated++;
|
|
1078
|
+
updated += await applyMissingRoleSections(existing, role, cubeId, active, authority.connection);
|
|
1079
|
+
}
|
|
1080
|
+
for (const classDef of template.message_taxonomy ?? []) {
|
|
1081
|
+
const currentClasses = (current.message_taxonomy ?? []);
|
|
1082
|
+
if (!currentClasses.some((candidate) => candidate.class === classDef.class)) {
|
|
1083
|
+
await patchTaxonomyClass(cubeId, { action: 'add', class_def: classDef }, active, authority.connection);
|
|
1084
|
+
updated++;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return { created, updated };
|
|
963
1088
|
}
|
|
964
1089
|
/**
|
|
965
1090
|
* gh#473 PR2 — NON-CLOBBERING sync of a cube's roles + message_taxonomy
|
|
@@ -972,11 +1097,166 @@ export async function applyTemplate(cubeId, templateName) {
|
|
|
972
1097
|
* never silently overwritten. Custom roles (names not in template) are
|
|
973
1098
|
* never touched. Returns a NonClobberSyncResult.
|
|
974
1099
|
*/
|
|
975
|
-
export async function syncRoles(cubeId, templateName = 'software-dev', apply = false, decisions) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1100
|
+
export async function syncRoles(cubeId, templateName = 'software-dev', apply = false, decisions, authorityOverride) {
|
|
1101
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
1102
|
+
const template = getTemplate(templateName);
|
|
1103
|
+
if (!template)
|
|
1104
|
+
throw new Error(`Unknown Borg template ${JSON.stringify(templateName)}`);
|
|
1105
|
+
const active = authorityOverride?.active ?? await getActiveCube();
|
|
1106
|
+
if (!active?.serverTrustIdentity)
|
|
1107
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1108
|
+
const authority = authorityOverride ?? await resolveLocalManageAuthority(active, {
|
|
1109
|
+
operation: `sync template ${manageCopyValue(templateName)}`,
|
|
1110
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1111
|
+
noMutation: 'No role synchronization changes were applied.',
|
|
1112
|
+
});
|
|
1113
|
+
const current = await getCubeForManagement(cubeId, {
|
|
1114
|
+
operation: `sync template ${manageCopyValue(templateName)}`,
|
|
1115
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1116
|
+
noMutation: 'No role synchronization changes were applied.',
|
|
1117
|
+
}, active, authority.connection);
|
|
1118
|
+
const roles = [];
|
|
1119
|
+
const taxonomy = [];
|
|
1120
|
+
const additions = [];
|
|
1121
|
+
const conflictKeys = new Set();
|
|
1122
|
+
for (const role of template.roles) {
|
|
1123
|
+
const existing = current.roles.find((candidate) => candidate.name === role.name);
|
|
1124
|
+
if (!existing) {
|
|
1125
|
+
const key = `role:${role.name}`;
|
|
1126
|
+
const fragments = [{
|
|
1127
|
+
key,
|
|
1128
|
+
kind: 'add',
|
|
1129
|
+
label: 'role',
|
|
1130
|
+
cubeValue: null,
|
|
1131
|
+
templateValue: role.name,
|
|
1132
|
+
}];
|
|
1133
|
+
roles.push({ name: role.name, status: 'new', fragments });
|
|
1134
|
+
additions.push({ key, run: async () => { await createRole(cubeId, role, active, authority.connection); } });
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const fragments = [];
|
|
1138
|
+
addRoleScalarFragment(fragments, additions, conflictKeys, decisions, existing, role, 'short_description', 'short description', cubeId, active, authority.connection);
|
|
1139
|
+
for (const field of ['is_default', 'is_mandatory', 'is_human_seat', 'can_broadcast', 'receives_all_direct']) {
|
|
1140
|
+
if (role[field] !== undefined)
|
|
1141
|
+
addRoleScalarFragment(fragments, additions, conflictKeys, decisions, existing, role, field, field, cubeId, active, authority.connection);
|
|
1142
|
+
}
|
|
1143
|
+
const currentSections = new Map(parseRoleSections(String(existing.detailed_description ?? '')).map((section) => [section.heading, section]));
|
|
1144
|
+
for (const section of parseRoleSections(role.detailed_description)) {
|
|
1145
|
+
if (!section.heading)
|
|
1146
|
+
continue;
|
|
1147
|
+
const key = `role:${role.name}:section:${section.heading}`;
|
|
1148
|
+
const previous = currentSections.get(section.heading);
|
|
1149
|
+
const kind = !previous ? 'add' : previous.body === section.body ? 'unchanged' : 'conflict';
|
|
1150
|
+
fragments.push({ key, kind, label: `section ${section.heading}`, cubeValue: previous?.body ?? null, templateValue: section.body });
|
|
1151
|
+
if (kind === 'add')
|
|
1152
|
+
additions.push({ key, run: async () => { await patchRoleSection(existing.id, { action: 'insert', heading: section.heading, body: section.body }, cubeId, active, authority.connection); } });
|
|
1153
|
+
if (kind === 'conflict')
|
|
1154
|
+
conflictKeys.add(key);
|
|
1155
|
+
if (kind === 'conflict' && decisions?.[key] === 'accept')
|
|
1156
|
+
additions.push({ key, run: async () => { await patchRoleSection(existing.id, { action: 'replace', heading: section.heading, body: section.body }, cubeId, active, authority.connection); } });
|
|
1157
|
+
}
|
|
1158
|
+
roles.push({ name: role.name, status: 'existing', fragments });
|
|
1159
|
+
}
|
|
1160
|
+
for (const existing of current.roles) {
|
|
1161
|
+
if (!template.roles.some((role) => role.name === existing.name)) {
|
|
1162
|
+
roles.push({ name: existing.name, status: 'custom-skipped', fragments: [] });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
for (const classDef of template.message_taxonomy ?? []) {
|
|
1166
|
+
const key = `taxonomy:${classDef.class}`;
|
|
1167
|
+
const currentClass = (current.message_taxonomy ?? []).find((candidate) => candidate.class === classDef.class);
|
|
1168
|
+
const currentValue = currentClass ? stableJson(currentClass) : null;
|
|
1169
|
+
const templateValue = stableJson(classDef);
|
|
1170
|
+
const kind = !currentClass ? 'add' : currentValue === templateValue ? 'unchanged' : 'conflict';
|
|
1171
|
+
taxonomy.push({ key, kind, label: `taxonomy class ${classDef.class}`, cubeValue: currentValue, templateValue });
|
|
1172
|
+
if (kind === 'add')
|
|
1173
|
+
additions.push({ key, run: async () => { await patchTaxonomyClass(cubeId, { action: 'add', class_def: classDef }, active, authority.connection); } });
|
|
1174
|
+
if (kind === 'conflict')
|
|
1175
|
+
conflictKeys.add(key);
|
|
1176
|
+
if (kind === 'conflict' && decisions?.[key] === 'accept')
|
|
1177
|
+
additions.push({ key, run: async () => { await patchTaxonomyClass(cubeId, { action: 'replace', class_def: classDef }, active, authority.connection); } });
|
|
1178
|
+
}
|
|
1179
|
+
const acceptedConflicts = [...conflictKeys].filter((key) => decisions?.[key] === 'accept');
|
|
1180
|
+
const rejectedConflicts = [...conflictKeys].filter((key) => decisions?.[key] !== 'accept');
|
|
1181
|
+
const classifiedKeys = new Set([...conflictKeys]);
|
|
1182
|
+
const unmatchedDecisions = Object.keys(decisions ?? {}).filter((key) => !classifiedKeys.has(key));
|
|
1183
|
+
const addedKeys = additions.filter(({ key }) => !conflictKeys.has(key)).map(({ key }) => key);
|
|
1184
|
+
if (apply) {
|
|
1185
|
+
for (const addition of additions) {
|
|
1186
|
+
if (conflictKeys.has(addition.key) && decisions?.[addition.key] !== 'accept')
|
|
1187
|
+
continue;
|
|
1188
|
+
await addition.run();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
dryRun: !apply,
|
|
1193
|
+
roles,
|
|
1194
|
+
taxonomy,
|
|
1195
|
+
applied: {
|
|
1196
|
+
added: apply ? addedKeys : [],
|
|
1197
|
+
acceptedConflicts: apply ? acceptedConflicts : [],
|
|
1198
|
+
},
|
|
1199
|
+
rejectedConflicts,
|
|
1200
|
+
unmatchedDecisions,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function stableJson(value) {
|
|
1204
|
+
if (Array.isArray(value))
|
|
1205
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
1206
|
+
if (value !== null && typeof value === 'object') {
|
|
1207
|
+
return `{${Object.entries(value)
|
|
1208
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1209
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
|
|
1210
|
+
.join(',')}}`;
|
|
1211
|
+
}
|
|
1212
|
+
return JSON.stringify(value);
|
|
1213
|
+
}
|
|
1214
|
+
function addRoleScalarFragment(fragments, additions, conflictKeys, decisions, existing, template, field, label, cubeId, active, connection) {
|
|
1215
|
+
const templateValue = template[field];
|
|
1216
|
+
if (templateValue === undefined)
|
|
1217
|
+
return;
|
|
1218
|
+
const key = `role:${template.name}:${field}`;
|
|
1219
|
+
const currentValue = existing[field];
|
|
1220
|
+
const missing = currentValue === undefined || (field === 'short_description' && currentValue === '');
|
|
1221
|
+
const kind = missing ? 'add' : currentValue === templateValue ? 'unchanged' : 'conflict';
|
|
1222
|
+
fragments.push({ key, kind, label, cubeValue: missing ? null : String(currentValue), templateValue: String(templateValue) });
|
|
1223
|
+
if (kind === 'add')
|
|
1224
|
+
additions.push({
|
|
1225
|
+
key,
|
|
1226
|
+
run: async () => { await updateRole(existing.id, { [field]: templateValue }, cubeId, active, connection); },
|
|
1227
|
+
});
|
|
1228
|
+
if (kind === 'conflict') {
|
|
1229
|
+
conflictKeys.add(key);
|
|
1230
|
+
if (decisions?.[key] === 'accept')
|
|
1231
|
+
additions.push({
|
|
1232
|
+
key,
|
|
1233
|
+
run: async () => { await updateRole(existing.id, { [field]: templateValue }, cubeId, active, connection); },
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
async function applyMissingRoleFields(existing, template, cubeId, active, connection) {
|
|
1238
|
+
const updates = {};
|
|
1239
|
+
if ((existing.short_description === undefined || existing.short_description === '') && template.short_description) {
|
|
1240
|
+
updates.short_description = template.short_description;
|
|
1241
|
+
}
|
|
1242
|
+
for (const field of ['is_default', 'is_mandatory', 'is_human_seat', 'can_broadcast', 'receives_all_direct']) {
|
|
1243
|
+
if (existing[field] === undefined && template[field] !== undefined)
|
|
1244
|
+
updates[field] = template[field];
|
|
1245
|
+
}
|
|
1246
|
+
if (Object.keys(updates).length === 0)
|
|
1247
|
+
return false;
|
|
1248
|
+
await updateRole(existing.id, updates, cubeId, active, connection);
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1251
|
+
async function applyMissingRoleSections(existing, template, cubeId, active, connection) {
|
|
1252
|
+
const currentSections = new Map(parseRoleSections(String(existing.detailed_description ?? '')).map((section) => [section.heading, section]));
|
|
1253
|
+
let updated = 0;
|
|
1254
|
+
for (const section of parseRoleSections(template.detailed_description)) {
|
|
1255
|
+
if (section.heading && !currentSections.has(section.heading)) {
|
|
1256
|
+
await patchRoleSection(existing.id, { action: 'insert', heading: section.heading, body: section.body }, cubeId, active, connection);
|
|
1257
|
+
updated++;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return updated;
|
|
981
1261
|
}
|
|
982
1262
|
//# sourceMappingURL=remote-client.js.map
|