borgmcp 2.0.9 → 2.0.11
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/SECURITY.md +1 -1
- 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.map +1 -1
- package/dist/index.js +18 -17
- package/dist/index.js.map +1 -1
- package/dist/local-manage-tool-result.d.ts +10 -0
- package/dist/local-manage-tool-result.d.ts.map +1 -1
- package/dist/local-manage-tool-result.js +36 -0
- package/dist/local-manage-tool-result.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 +18 -6
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +99 -16
- 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/tool-manifest.js +2 -2
- package/dist/tool-manifest.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 -8
- package/docs/LOCAL_SERVER.md +1 -1
- package/docs/RELEASING.md +25 -13
- 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 +26 -21
- package/src/local-manage-tool-result.ts +47 -0
- package/src/regen-format.ts +12 -5
- package/src/regen.ts +2 -0
- package/src/remote-client.ts +124 -24
- package/src/roster-render.ts +58 -8
- package/src/runtime-metadata.ts +67 -0
- package/src/server-handshake.ts +7 -2
- package/src/tool-manifest.ts +2 -2
- package/src/working-repo.ts +30 -41
package/src/assimilate-cmd.ts
CHANGED
|
@@ -49,6 +49,7 @@ import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } f
|
|
|
49
49
|
import type { SeatBinding, BindPendingSeatOutcome } from './seats.js';
|
|
50
50
|
import { createHash } from 'node:crypto';
|
|
51
51
|
import { buildOpenCodeLaunchArgs, type LaunchApprovalDecision } from './cli-tool-approval.js';
|
|
52
|
+
import { resolveWorkingRepo, type WorkingRepo } from './working-repo.js';
|
|
52
53
|
|
|
53
54
|
const PRIVATE_STATE_UNAVAILABLE_COPY = [
|
|
54
55
|
'Borg could not safely prepare its private local state.',
|
|
@@ -259,7 +260,7 @@ export interface AssimilateDeps {
|
|
|
259
260
|
assimilate: (
|
|
260
261
|
apiUrl: string,
|
|
261
262
|
token: string,
|
|
262
|
-
params: { cube_id: string; role_id: string; hostname?: string | null; prior_drone_id?: string; remint_invalid_prior?: boolean; model?: string | null; agent_kind?: 'claude' | 'codex' | 'opencode' | null; session_operation?: ServerSessionOperation; session_expected?: ExpectedBinding; revalidate_at_prepare?: boolean },
|
|
263
|
+
params: { cube_id: string; role_id: string; hostname?: string | null; prior_drone_id?: string; remint_invalid_prior?: boolean; model?: string | null; agent_kind?: 'claude' | 'codex' | 'opencode' | null; working_repo?: WorkingRepo; session_operation?: ServerSessionOperation; session_expected?: ExpectedBinding; revalidate_at_prepare?: boolean },
|
|
263
264
|
serverTrustIdentity?: string,
|
|
264
265
|
) => Promise<AssimilateResult>;
|
|
265
266
|
|
|
@@ -1183,6 +1184,7 @@ export async function runAssimilate(
|
|
|
1183
1184
|
hostname: deps.getHostname(),
|
|
1184
1185
|
agent_kind: cli,
|
|
1185
1186
|
model: effectiveModel,
|
|
1187
|
+
working_repo: resolveWorkingRepo(projectRoot),
|
|
1186
1188
|
...(reattachPriorId ? { prior_drone_id: reattachPriorId } : {}),
|
|
1187
1189
|
...(remintInvalidPrior ? { remint_invalid_prior: true } : {}),
|
|
1188
1190
|
session_operation: sessionOperation,
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
16
16
|
import prompts from 'prompts';
|
|
17
17
|
import { readinessProbeEnv } from './readiness-probe.js';
|
|
18
18
|
import { resolveMcpBinaryPath } from './self-path.js';
|
|
19
|
+
import { buildRuntimeMetadataReport } from './runtime-metadata.js';
|
|
19
20
|
|
|
20
21
|
import type { AssimilateDeps } from './assimilate-cmd.js';
|
|
21
22
|
import {
|
|
@@ -322,10 +323,15 @@ export function buildDefaultAssimilateDeps(): AssimilateDeps {
|
|
|
322
323
|
cubeId: params.cube_id,
|
|
323
324
|
roleId: params.role_id,
|
|
324
325
|
operation,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
...(params.prior_drone_id === undefined
|
|
327
|
+
? {}
|
|
328
|
+
: { priorDroneId: params.prior_drone_id }),
|
|
329
|
+
runtimeMetadata: buildRuntimeMetadataReport({
|
|
330
|
+
agentKind: params.agent_kind,
|
|
331
|
+
reportedModel: params.model,
|
|
332
|
+
workingRepo: params.working_repo,
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
329
335
|
pending.credential,
|
|
330
336
|
{ fetchImpl: trust.fetchImpl },
|
|
331
337
|
);
|
package/src/index.ts
CHANGED
|
@@ -93,7 +93,11 @@ import {
|
|
|
93
93
|
formatWakePathPrefix,
|
|
94
94
|
shouldShowWakePathWarning,
|
|
95
95
|
} from './stream-status.js';
|
|
96
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
RUNTIME_METADATA_ADVISORY,
|
|
98
|
+
renderRoster,
|
|
99
|
+
renderRuntimeMetadataLines,
|
|
100
|
+
} from './roster-render.js';
|
|
97
101
|
import { resolveWorkingRepo } from './working-repo.js';
|
|
98
102
|
import {
|
|
99
103
|
DroneEvictedError,
|
|
@@ -110,6 +114,7 @@ import { initConsolePrefix, consolePrefix } from './console-prefix.js';
|
|
|
110
114
|
import {
|
|
111
115
|
resolveSessionAgentKind,
|
|
112
116
|
} from './codex-app-wake.js';
|
|
117
|
+
import { resolveReportableSessionAgentKind } from './agent-runtime.js';
|
|
113
118
|
import {
|
|
114
119
|
connectOpenCodeDrone,
|
|
115
120
|
injectOpenCodeEntry,
|
|
@@ -134,14 +139,15 @@ import {
|
|
|
134
139
|
/**
|
|
135
140
|
* Apply a template's roles + message_taxonomy to a cube.
|
|
136
141
|
*
|
|
137
|
-
*
|
|
138
|
-
* are inserted; existing template-named roles get ADD
|
|
139
|
-
* sections/classes the cube lacks) auto-applied, but
|
|
140
|
-
* fragments are
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
142
|
+
* Client-orchestrated through the server's non-clobbering role and taxonomy
|
|
143
|
+
* primitives. New roles are inserted; existing template-named roles get ADD
|
|
144
|
+
* fragments (template sections/classes the cube lacks) auto-applied, but
|
|
145
|
+
* EVOLVED (conflicting) fragments are kept — never silently overwritten. The
|
|
146
|
+
* old per-role blanket `updateRole`/whole-taxonomy `updateCube` overwrite path
|
|
147
|
+
* is removed. Operators who want to take the template version of a conflicting
|
|
148
|
+
* fragment use `borg_sync-roles` with a `decisions` map. Primitive operations
|
|
149
|
+
* are sequential, so a later failure can leave earlier changes committed.
|
|
150
|
+
* Returns `{ created, updated }` for the caller's toast.
|
|
145
151
|
*/
|
|
146
152
|
async function applyTemplateToCube(
|
|
147
153
|
cubeId: string,
|
|
@@ -368,6 +374,7 @@ export async function main() {
|
|
|
368
374
|
const result = await regen(active.sessionToken, active.apiUrl, {
|
|
369
375
|
since,
|
|
370
376
|
reportedModel,
|
|
377
|
+
agentKind: resolveReportableSessionAgentKind(),
|
|
371
378
|
workingRepo: resolveWorkingRepo(),
|
|
372
379
|
serverTrustIdentity: active.serverTrustIdentity,
|
|
373
380
|
});
|
|
@@ -444,6 +451,7 @@ export async function main() {
|
|
|
444
451
|
// — an evicted/revoked seat FAILS server-side and is surfaced).
|
|
445
452
|
try {
|
|
446
453
|
const result = await regen(active!.sessionToken, active!.apiUrl, {
|
|
454
|
+
agentKind: resolveReportableSessionAgentKind(),
|
|
447
455
|
workingRepo: resolveWorkingRepo(),
|
|
448
456
|
serverTrustIdentity: active!.serverTrustIdentity,
|
|
449
457
|
});
|
|
@@ -1084,21 +1092,18 @@ export async function main() {
|
|
|
1084
1092
|
return { content: [{ type: 'text', text: 'No drones in this cube yet.' }] };
|
|
1085
1093
|
}
|
|
1086
1094
|
const rolesById = new Map(roles.map((r: any) => [r.id, r]));
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const wakeClass =
|
|
1095
|
+
const lines = drones.map((d: any) => {
|
|
1096
|
+
const r = rolesById.get(d.role_id) as any;
|
|
1097
|
+
const wakeClass =
|
|
1091
1098
|
d.wake_path_alert_class && d.wake_path_alert_class !== 'independent'
|
|
1092
1099
|
? ` — wake-path-class: ${d.wake_path_alert_class}`
|
|
1093
1100
|
: '';
|
|
1094
|
-
|
|
1095
|
-
?
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
});
|
|
1101
|
-
return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n${lines.join('\n')}` }] };
|
|
1101
|
+
return [
|
|
1102
|
+
`- **${d.label}** (id: ${d.id}) — Role: ${r?.name ?? '?'} (${d.role_id}) — last seen ${d.last_seen}${wakeClass}`,
|
|
1103
|
+
...renderRuntimeMetadataLines(d),
|
|
1104
|
+
].join('\n');
|
|
1105
|
+
});
|
|
1106
|
+
return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n_${RUNTIME_METADATA_ADVISORY}_\n\n${lines.join('\n')}` }] };
|
|
1102
1107
|
}
|
|
1103
1108
|
|
|
1104
1109
|
case 'borg_list-roles': {
|
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import { LocalManageRequiredError } from './server-errors.js';
|
|
2
2
|
|
|
3
|
+
export interface RoleSectionConflictOperation {
|
|
4
|
+
roleId: string;
|
|
5
|
+
action: 'replace' | 'insert' | 'delete';
|
|
6
|
+
heading: string;
|
|
7
|
+
after?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RoleSectionConflictError extends Error {
|
|
11
|
+
constructor(public readonly operation: RoleSectionConflictOperation) {
|
|
12
|
+
super('The role section patch conflicted with the current role text.');
|
|
13
|
+
this.name = 'RoleSectionConflictError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const UNSAFE_OPERATION_CHARACTER =
|
|
18
|
+
/["\\`*_[\]()<>#!|~\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/u;
|
|
19
|
+
|
|
20
|
+
function safeOperationValue(value: string): string {
|
|
21
|
+
const escaped = Array.from(value, (character) =>
|
|
22
|
+
UNSAFE_OPERATION_CHARACTER.test(character)
|
|
23
|
+
? `\\u{${character.codePointAt(0)!.toString(16)}}`
|
|
24
|
+
: character).join('');
|
|
25
|
+
return `"${escaped}"`;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
export function formatLocalManageToolResult(error: unknown) {
|
|
29
|
+
if (error instanceof RoleSectionConflictError) {
|
|
30
|
+
const { roleId, action, heading, after } = error.operation;
|
|
31
|
+
const context = [
|
|
32
|
+
`action=${action}`,
|
|
33
|
+
`heading=${safeOperationValue(heading)}`,
|
|
34
|
+
...(action === 'insert'
|
|
35
|
+
? [`after=${after == null ? '<end>' : safeOperationValue(after)}`]
|
|
36
|
+
: []),
|
|
37
|
+
].join(', ');
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: 'text' as const,
|
|
41
|
+
text:
|
|
42
|
+
`[ROLE-SECTION-CONFLICT] The requested role section operation conflicts with the current role text.\n\n` +
|
|
43
|
+
`Requested operation: ${context}\n\n` +
|
|
44
|
+
'No role text was changed by this request. ' +
|
|
45
|
+
`Read the current role with \`borg_role\` using role id ${safeOperationValue(roleId)}, ` +
|
|
46
|
+
'then retry `borg_patch-role-section` against that current text.',
|
|
47
|
+
}],
|
|
48
|
+
isError: true as const,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
4
51
|
if (!(error instanceof LocalManageRequiredError)) return null;
|
|
5
52
|
|
|
6
53
|
return {
|
package/src/regen-format.ts
CHANGED
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
} from 'borgmcp-shared/templates';
|
|
13
13
|
import { parseRoleSections } from 'borgmcp-shared/role-section';
|
|
14
14
|
import { formatDroneAddressToken } from 'borgmcp-shared/drone-address';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
RUNTIME_METADATA_ADVISORY,
|
|
17
|
+
renderRuntimeMetadataLines,
|
|
18
|
+
} from './roster-render.js';
|
|
16
19
|
import { shellEscape } from './shell-escape.js';
|
|
17
20
|
import { resolveInboxMonitorPath } from './self-path.js';
|
|
18
21
|
|
|
@@ -488,10 +491,12 @@ export function formatRegenMarkdown(
|
|
|
488
491
|
|
|
489
492
|
const droneOverview =
|
|
490
493
|
result.drones
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
494
|
+
.map((d: any) => {
|
|
495
|
+
const role = result.roles.find((r: any) => r.id === d.role_id);
|
|
496
|
+
return [
|
|
497
|
+
`- **${d.label}** (Role: ${role?.name ?? '?'}) — last seen ${humanAgo(new Date(d.last_seen))}`,
|
|
498
|
+
...renderRuntimeMetadataLines(d),
|
|
499
|
+
].join('\n');
|
|
495
500
|
})
|
|
496
501
|
.join('\n') || '_(no drones connected)_';
|
|
497
502
|
|
|
@@ -611,6 +616,8 @@ export function formatRegenMarkdown(
|
|
|
611
616
|
roleOverview,
|
|
612
617
|
'',
|
|
613
618
|
`## Connected drones`,
|
|
619
|
+
`_${RUNTIME_METADATA_ADVISORY}_`,
|
|
620
|
+
'',
|
|
614
621
|
droneOverview,
|
|
615
622
|
'',
|
|
616
623
|
`## Cube log`,
|
package/src/regen.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type AgentKind,
|
|
27
27
|
} from './regen-format.js';
|
|
28
28
|
import { resolveSessionAgentKind } from './codex-app-wake.js';
|
|
29
|
+
import { resolveReportableSessionAgentKind } from './agent-runtime.js';
|
|
29
30
|
import { handleVersionFlag } from './version.js';
|
|
30
31
|
import { gateAllowsActivation } from './launch-gate.js';
|
|
31
32
|
import { resolveWorkingRepo } from './working-repo.js';
|
|
@@ -85,6 +86,7 @@ async function main(): Promise<void> {
|
|
|
85
86
|
let result: Awaited<ReturnType<typeof regen>> | null = null;
|
|
86
87
|
try {
|
|
87
88
|
result = await regen(active.sessionToken, active.apiUrl, {
|
|
89
|
+
agentKind: resolveReportableSessionAgentKind(),
|
|
88
90
|
workingRepo: resolveWorkingRepo(),
|
|
89
91
|
serverTrustIdentity: active.serverTrustIdentity,
|
|
90
92
|
});
|
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';
|
|
@@ -33,6 +36,7 @@ import { getTemplate, type Template, type TemplateRole } from 'borgmcp-shared/te
|
|
|
33
36
|
import { parseRoleSections } from 'borgmcp-shared/role-section';
|
|
34
37
|
import type { FragmentView, NonClobberSyncResult } from './sync-roles-render.js';
|
|
35
38
|
import type { WorkingRepo } from './working-repo.js';
|
|
39
|
+
import { buildRuntimeMetadataPatch } from './runtime-metadata.js';
|
|
36
40
|
import { loadBorgServerTrust, type ServerFetch } from './server-trust.js';
|
|
37
41
|
import {
|
|
38
42
|
BorgServerError,
|
|
@@ -50,6 +54,7 @@ import {
|
|
|
50
54
|
} from './local-server-cursor.js';
|
|
51
55
|
import { readBoundedResponseBody } from './server-response.js';
|
|
52
56
|
import { resolveLocalLogRecipients } from './local-log-routing.js';
|
|
57
|
+
import { RoleSectionConflictError } from './local-manage-tool-result.js';
|
|
53
58
|
|
|
54
59
|
export interface RemoteConnection {
|
|
55
60
|
apiUrl: string;
|
|
@@ -67,6 +72,7 @@ export const LOCAL_SERVER_RESPONSE_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
|
67
72
|
// A typed auth-error envelope is tiny; anything larger is hostile and the
|
|
68
73
|
// bounded read throws → the 401 fails closed to non-destructive CREDENTIAL_REJECTED.
|
|
69
74
|
const AUTH_ERROR_ENVELOPE_LIMIT_BYTES = 64 * 1024;
|
|
75
|
+
const ROLE_SECTION_CONFLICT_CODE = 'ROLE_SECTION_CONFLICT';
|
|
70
76
|
export const LOCAL_SERVER_REQUEST_TIMEOUT_MS = 5_000;
|
|
71
77
|
const LOCAL_SERVER_RESPONSE_LIMIT_MESSAGE =
|
|
72
78
|
'Local Borg server response exceeded the response limit';
|
|
@@ -400,18 +406,42 @@ async function localCubeComposition(active: ActiveCube): Promise<{
|
|
|
400
406
|
if (!cubePayload || !rolePayload || !dronePayload) {
|
|
401
407
|
throw new Error('Local Borg server returned an incomplete cube response');
|
|
402
408
|
}
|
|
403
|
-
const
|
|
409
|
+
const drones = dronePayload.drones.map(withValidatedRuntimeMetadata);
|
|
410
|
+
const drone = drones.find((candidate) => candidate.id === active.droneId);
|
|
404
411
|
const role = rolePayload.roles.find((candidate) => candidate.id === drone?.role_id);
|
|
405
412
|
if (!drone || !role) throw new Error('Local Borg server no longer recognizes this drone seat');
|
|
406
413
|
return {
|
|
407
414
|
cube: cubePayload.cube,
|
|
408
415
|
roles: rolePayload.roles,
|
|
409
|
-
drones
|
|
416
|
+
drones,
|
|
410
417
|
role,
|
|
411
418
|
drone,
|
|
412
419
|
};
|
|
413
420
|
}
|
|
414
421
|
|
|
422
|
+
function withValidatedRuntimeMetadata<T extends Record<string, unknown>>(drone: T): T {
|
|
423
|
+
const state = decodeDroneRuntimeMetadataState(drone);
|
|
424
|
+
return {
|
|
425
|
+
...drone,
|
|
426
|
+
...state.runtime_metadata,
|
|
427
|
+
runtime_metadata_reported: state.runtime_metadata_reported,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function updateOwnRuntimeMetadata(
|
|
432
|
+
active: ActiveCube,
|
|
433
|
+
patch: ReturnType<typeof buildRuntimeMetadataPatch>,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const payload = await localServerRequest<Record<string, unknown>>(
|
|
436
|
+
active,
|
|
437
|
+
`/api/cubes/${active.cubeId}/drones/self/metadata`,
|
|
438
|
+
'PATCH',
|
|
439
|
+
{ ...patch },
|
|
440
|
+
);
|
|
441
|
+
if (!payload) throw new Error('Local Borg server returned an empty runtime metadata response');
|
|
442
|
+
decodeUpdateDroneRuntimeMetadataResponse(payload);
|
|
443
|
+
}
|
|
444
|
+
|
|
415
445
|
function localCursorBinding(active: ActiveCube) {
|
|
416
446
|
return {
|
|
417
447
|
origin: active.apiUrl,
|
|
@@ -673,7 +703,30 @@ async function authedFetch(
|
|
|
673
703
|
AUTH_ERROR_ENVELOPE_LIMIT_BYTES,
|
|
674
704
|
'Local Borg server error response exceeded the response limit',
|
|
675
705
|
);
|
|
676
|
-
|
|
706
|
+
const parsed = JSON.parse(body);
|
|
707
|
+
try {
|
|
708
|
+
code = decodeProtocolErrorEnvelope(parsed).error.code;
|
|
709
|
+
} catch {
|
|
710
|
+
if (
|
|
711
|
+
parsed !== null && typeof parsed === 'object' &&
|
|
712
|
+
parsed.error !== null && typeof parsed.error === 'object' &&
|
|
713
|
+
parsed.error.code === ROLE_SECTION_CONFLICT_CODE
|
|
714
|
+
) {
|
|
715
|
+
// Shared 0.6.2 intentionally omits this server-local code. Re-validate the whole
|
|
716
|
+
// envelope through the strict shared decoder with only the recognized
|
|
717
|
+
// code substituted; no server-provided diagnostic is ever surfaced.
|
|
718
|
+
decodeProtocolErrorEnvelope({
|
|
719
|
+
...parsed,
|
|
720
|
+
error: {
|
|
721
|
+
...parsed.error,
|
|
722
|
+
code: ErrorCode.INVALID_INPUT,
|
|
723
|
+
message: 'Role section conflict.',
|
|
724
|
+
...(Object.hasOwn(parsed.error, 'details') ? { details: 'Redacted.' } : {}),
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
code = ROLE_SECTION_CONFLICT_CODE as ErrorCode;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
677
730
|
} catch {
|
|
678
731
|
code = undefined;
|
|
679
732
|
}
|
|
@@ -740,7 +793,7 @@ export async function whoami(
|
|
|
740
793
|
sessionToken: string,
|
|
741
794
|
apiUrl: string,
|
|
742
795
|
serverTrustIdentity?: string,
|
|
743
|
-
): Promise<{ cube_id: string; cube_name: string; drone_id: string; drone_label: string; role_id: string; role_name: string }> {
|
|
796
|
+
): 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 }> {
|
|
744
797
|
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
745
798
|
const composed = await localCubeComposition(local);
|
|
746
799
|
return {
|
|
@@ -750,6 +803,13 @@ export async function whoami(
|
|
|
750
803
|
drone_label: composed.drone.label,
|
|
751
804
|
role_id: composed.role.id,
|
|
752
805
|
role_name: composed.role.name,
|
|
806
|
+
runtime_metadata: {
|
|
807
|
+
agent_kind: composed.drone.agent_kind,
|
|
808
|
+
reported_model: composed.drone.reported_model,
|
|
809
|
+
working_repo_name: composed.drone.working_repo_name,
|
|
810
|
+
working_repo_origin: composed.drone.working_repo_origin,
|
|
811
|
+
},
|
|
812
|
+
runtime_metadata_reported: composed.drone.runtime_metadata_reported,
|
|
753
813
|
};
|
|
754
814
|
}
|
|
755
815
|
|
|
@@ -786,7 +846,7 @@ export async function getRoster(
|
|
|
786
846
|
throw new Error('Local Borg server returned an incomplete roster response');
|
|
787
847
|
}
|
|
788
848
|
return {
|
|
789
|
-
drones: dronePayload.drones,
|
|
849
|
+
drones: dronePayload.drones.map(withValidatedRuntimeMetadata),
|
|
790
850
|
roles: rolePayload.roles,
|
|
791
851
|
message_taxonomy: cubePayload.cube.message_taxonomy ?? null,
|
|
792
852
|
since: dronePayload.since ?? since,
|
|
@@ -944,6 +1004,8 @@ export async function regen(
|
|
|
944
1004
|
since?: string;
|
|
945
1005
|
/** Advisory self-report from the running agent; never model-routing config. */
|
|
946
1006
|
reportedModel?: string;
|
|
1007
|
+
/** Positively identified running Agent CLI; null means explicitly unknown. */
|
|
1008
|
+
agentKind?: AgentKind | null;
|
|
947
1009
|
/** Current cwd-derived identity; refreshed each regen to avoid stale routing data. */
|
|
948
1010
|
workingRepo?: WorkingRepo;
|
|
949
1011
|
/** Verified self-hosted authority from the caller's first active-state read. */
|
|
@@ -970,6 +1032,24 @@ export async function regen(
|
|
|
970
1032
|
apiUrl,
|
|
971
1033
|
opts.serverTrustIdentity,
|
|
972
1034
|
);
|
|
1035
|
+
if (
|
|
1036
|
+
opts.agentKind !== undefined ||
|
|
1037
|
+
opts.reportedModel !== undefined ||
|
|
1038
|
+
opts.workingRepo !== undefined
|
|
1039
|
+
) {
|
|
1040
|
+
const patch = buildRuntimeMetadataPatch({
|
|
1041
|
+
agentKind: opts.agentKind ?? null,
|
|
1042
|
+
reportedModel: opts.reportedModel,
|
|
1043
|
+
workingRepo: opts.workingRepo,
|
|
1044
|
+
});
|
|
1045
|
+
try {
|
|
1046
|
+
await updateOwnRuntimeMetadata(local, patch);
|
|
1047
|
+
} catch {
|
|
1048
|
+
// Metadata is advisory. Preserve the prior server value and continue with
|
|
1049
|
+
// the authenticated identity read; never echo rejected local input.
|
|
1050
|
+
console.warn('Local regen: runtime metadata update unavailable; preserving the prior safe report.');
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
973
1053
|
const composed = await localCubeComposition(local);
|
|
974
1054
|
const cursor = opts.since === undefined
|
|
975
1055
|
? await getLocalServerCursor(localCursorBinding(local))
|
|
@@ -1355,19 +1435,36 @@ export async function patchRoleSection(
|
|
|
1355
1435
|
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1356
1436
|
const cubeId = targetCubeId ?? active.cubeId;
|
|
1357
1437
|
assertUuidShape(cubeId, 'cube_id');
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1438
|
+
let result: { role: any } | null;
|
|
1439
|
+
try {
|
|
1440
|
+
result = await localManageRequest<{ role: any }>(
|
|
1441
|
+
active,
|
|
1442
|
+
`/api/cubes/${cubeId}/roles/${roleId}/section-patch`,
|
|
1443
|
+
'POST',
|
|
1444
|
+
{
|
|
1445
|
+
operation: `${op.action} section ${manageCopyValue(op.heading)} ${op.action === 'delete' ? 'from' : 'in'} role ${manageCopyValue(roleId)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
|
|
1446
|
+
cubeName: cubeId === active.cubeId ? active.name : cubeId,
|
|
1447
|
+
noMutation: `No role section was ${op.action === 'insert' ? 'inserted' : op.action === 'replace' ? 'replaced' : 'deleted'}.`,
|
|
1448
|
+
},
|
|
1449
|
+
{ ...op },
|
|
1450
|
+
undefined,
|
|
1451
|
+
connectionOverride,
|
|
1452
|
+
);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
if (
|
|
1455
|
+
error instanceof BorgServerHttpError &&
|
|
1456
|
+
error.status === 409 &&
|
|
1457
|
+
String(error.code) === ROLE_SECTION_CONFLICT_CODE
|
|
1458
|
+
) {
|
|
1459
|
+
throw new RoleSectionConflictError({
|
|
1460
|
+
roleId,
|
|
1461
|
+
action: op.action,
|
|
1462
|
+
heading: op.heading,
|
|
1463
|
+
...(op.action === 'insert' ? { after: op.after } : {}),
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
throw error;
|
|
1467
|
+
}
|
|
1371
1468
|
if (!result) throw new Error('Local Borg server returned an empty role response');
|
|
1372
1469
|
return result;
|
|
1373
1470
|
}
|
|
@@ -1494,16 +1591,17 @@ export async function getCube(cubeId: string, connection?: RemoteConnection): Pr
|
|
|
1494
1591
|
return {
|
|
1495
1592
|
...cubePayload.cube,
|
|
1496
1593
|
roles: rolePayload.roles,
|
|
1497
|
-
drones: dronePayload.drones,
|
|
1594
|
+
drones: dronePayload.drones.map(withValidatedRuntimeMetadata),
|
|
1498
1595
|
};
|
|
1499
1596
|
}
|
|
1500
1597
|
|
|
1501
1598
|
/**
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1599
|
+
* Apply a named template through client-orchestrated, non-clobbering role and
|
|
1600
|
+
* taxonomy primitives. New roles are inserted; existing
|
|
1504
1601
|
* template-named roles get ADD fragments auto-applied (template
|
|
1505
1602
|
* sections/classes the cube lacks) but their EVOLVED (conflicting)
|
|
1506
|
-
* fragments are
|
|
1603
|
+
* fragments are kept, never overwritten. Primitive operations are sequential,
|
|
1604
|
+
* so earlier changes may remain committed if a later operation fails. Returns
|
|
1507
1605
|
* `{ created, updated }` counts. To selectively take template versions of
|
|
1508
1606
|
* conflicting fragments, use `syncRoles` with a `decisions` map instead.
|
|
1509
1607
|
*/
|
|
@@ -1550,7 +1648,7 @@ export async function applyTemplate(
|
|
|
1550
1648
|
}
|
|
1551
1649
|
|
|
1552
1650
|
/**
|
|
1553
|
-
*
|
|
1651
|
+
* Client-orchestrated, non-clobbering sync of a cube's roles and taxonomy
|
|
1554
1652
|
* against the current built-in template. Dry-run by default classifies
|
|
1555
1653
|
* each fragment (role-text SECTION / short_description / flags / taxonomy
|
|
1556
1654
|
* CLASS) as ADD / UNCHANGED / CONFLICT. Pass apply=true to commit:
|
|
@@ -1558,7 +1656,9 @@ export async function applyTemplate(
|
|
|
1558
1656
|
* ONLY when their stable key appears in `decisions` as 'accept'.
|
|
1559
1657
|
* Unspecified conflicts DEFAULT TO REJECT — the cube's evolved text is
|
|
1560
1658
|
* never silently overwritten. Custom roles (names not in template) are
|
|
1561
|
-
* never touched.
|
|
1659
|
+
* never touched. Applied primitives are sequential rather than atomic, so an
|
|
1660
|
+
* earlier operation may remain committed if a later operation fails. Returns a
|
|
1661
|
+
* NonClobberSyncResult.
|
|
1562
1662
|
*/
|
|
1563
1663
|
export async function syncRoles(
|
|
1564
1664
|
cubeId: string,
|
package/src/roster-render.ts
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { formatDroneAddressToken } from 'borgmcp-shared/drone-address';
|
|
19
|
+
import { escapeSyncDisplay } from './sync-roles-render.js';
|
|
20
|
+
|
|
21
|
+
export const RUNTIME_METADATA_ADVISORY =
|
|
22
|
+
'Agent CLI, reported model, and working repository are advisory. They do not determine authority, role, health, activity, wake behavior, or routing.';
|
|
19
23
|
|
|
20
24
|
export interface RosterDrone {
|
|
21
25
|
id?: string;
|
|
@@ -47,6 +51,7 @@ export interface RosterDrone {
|
|
|
47
51
|
/** Current cwd-derived repository identity, refreshed on regen. */
|
|
48
52
|
working_repo_name?: string | null;
|
|
49
53
|
working_repo_origin?: string | null;
|
|
54
|
+
runtime_metadata_reported?: boolean;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
export interface RosterRole {
|
|
@@ -104,6 +109,54 @@ export function formatWorkingRepoLabel(drone: Pick<RosterDrone, 'working_repo_na
|
|
|
104
109
|
return 'Working repo: not reported';
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
function metadataValue(
|
|
113
|
+
drone: RosterDrone,
|
|
114
|
+
value: string | null | undefined,
|
|
115
|
+
): string {
|
|
116
|
+
if (drone.runtime_metadata_reported !== true) return 'not reported';
|
|
117
|
+
return value == null ? 'unknown' : escapeRuntimeMetadataDisplay(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Keep accepted advisory metadata readable without letting a Markdown renderer
|
|
122
|
+
* or link-detecting terminal turn cube-controlled text into a live target.
|
|
123
|
+
* The visible `[.]` / `[:]` markers preserve the reported value's differences.
|
|
124
|
+
*/
|
|
125
|
+
export function escapeRuntimeMetadataDisplay(value: string): string {
|
|
126
|
+
const escaped = escapeSyncDisplay(value);
|
|
127
|
+
const defangedScheme = escaped.replace(
|
|
128
|
+
/\b([A-Za-z][A-Za-z0-9+.-]*)\:\/\//g,
|
|
129
|
+
'$1\\[:]//',
|
|
130
|
+
);
|
|
131
|
+
return defangedScheme.replace(
|
|
132
|
+
/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,63}\b/g,
|
|
133
|
+
(host) => host.replaceAll('.', '\\[.\\]'),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function renderRuntimeMetadataLines(
|
|
138
|
+
drone: RosterDrone,
|
|
139
|
+
opts: { includeOrigin?: boolean } = {},
|
|
140
|
+
): string[] {
|
|
141
|
+
const agent = drone.agent_kind === 'claude'
|
|
142
|
+
? 'Claude Code'
|
|
143
|
+
: drone.agent_kind === 'codex'
|
|
144
|
+
? 'Codex'
|
|
145
|
+
: drone.agent_kind === 'opencode'
|
|
146
|
+
? 'OpenCode'
|
|
147
|
+
: null;
|
|
148
|
+
const lines = [
|
|
149
|
+
` - **Agent CLI:** ${metadataValue(drone, agent)}`,
|
|
150
|
+
` - **Reported model:** ${metadataValue(drone, drone.reported_model)}`,
|
|
151
|
+
` - **Working repo:** ${metadataValue(drone, drone.working_repo_name)}`,
|
|
152
|
+
];
|
|
153
|
+
if (opts.includeOrigin) {
|
|
154
|
+
const origin = drone.working_repo_origin?.replace(/^https:\/\//i, '');
|
|
155
|
+
lines.push(` - **Origin:** ${metadataValue(drone, origin)}`);
|
|
156
|
+
}
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
107
160
|
export interface RenderRosterInputs {
|
|
108
161
|
cubeName: string;
|
|
109
162
|
drones: RosterDrone[];
|
|
@@ -126,6 +179,8 @@ export function renderRoster(inputs: RenderRosterInputs): string {
|
|
|
126
179
|
const lines: string[] = [];
|
|
127
180
|
lines.push(`# Drones in cube: ${cubeName}`);
|
|
128
181
|
lines.push('');
|
|
182
|
+
lines.push(`_${RUNTIME_METADATA_ADVISORY}_`);
|
|
183
|
+
lines.push('');
|
|
129
184
|
|
|
130
185
|
if (resolvedSince) {
|
|
131
186
|
// Surface the liveness-probe context so the reader knows what the
|
|
@@ -146,15 +201,9 @@ export function renderRoster(inputs: RenderRosterInputs): string {
|
|
|
146
201
|
for (const d of drones) {
|
|
147
202
|
const role = roleById.get(d.role_id);
|
|
148
203
|
const roleName = role?.name ?? 'unknown';
|
|
149
|
-
const roleLabel = formatRoleAgentLabel(roleName, d.agent_kind);
|
|
150
204
|
// gh#371: stable short-uuid address token beside the (renumber-prone) label.
|
|
151
205
|
const addr = d.id ? ` ${formatDroneAddressToken(d.id)}` : '';
|
|
152
206
|
const lastSeen = humanAgo(d.last_seen);
|
|
153
|
-
const reportedModelMarker = d.reported_model
|
|
154
|
-
? ` · \`Reported model: ${d.reported_model}\``
|
|
155
|
-
: ' · `Reported model: not reported`';
|
|
156
|
-
const workingRepo = formatWorkingRepoLabel(d);
|
|
157
|
-
const workingRepoMarker = ` · \`${workingRepo}\``;
|
|
158
207
|
const wakePathMarker =
|
|
159
208
|
d.wake_path && d.wake_path !== 'live'
|
|
160
209
|
? ` · \`wake-path:${WAKE_PATH_DISPLAY[d.wake_path] ?? d.wake_path}\``
|
|
@@ -183,13 +232,14 @@ export function renderRoster(inputs: RenderRosterInputs): string {
|
|
|
183
232
|
const isAwake = d.seen_since === true;
|
|
184
233
|
const marker = isAwake ? '`awake`' : '`stale`';
|
|
185
234
|
lines.push(
|
|
186
|
-
`- **${d.label}**${addr} (${
|
|
235
|
+
`- **${d.label}**${addr} (Role: ${roleName}) — last seen ${lastSeen} · ${marker}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`
|
|
187
236
|
);
|
|
188
237
|
} else {
|
|
189
238
|
lines.push(
|
|
190
|
-
`- **${d.label}**${addr} (${
|
|
239
|
+
`- **${d.label}**${addr} (Role: ${roleName}) — last seen ${lastSeen}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`
|
|
191
240
|
);
|
|
192
241
|
}
|
|
242
|
+
lines.push(...renderRuntimeMetadataLines(d, { includeOrigin: true }));
|
|
193
243
|
}
|
|
194
244
|
|
|
195
245
|
return lines.join('\n');
|