borgmcp 2.0.9 → 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.map +1 -1
- package/dist/index.js +9 -9
- 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 +10 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +45 -5
- 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/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 +7 -7
- package/docs/LOCAL_SERVER.md +1 -1
- package/docs/RELEASING.md +11 -2
- 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 +17 -13
- package/src/regen-format.ts +12 -5
- package/src/regen.ts +2 -0
- package/src/remote-client.ts +60 -5
- package/src/roster-render.ts +58 -8
- package/src/runtime-metadata.ts +67 -0
- package/src/server-handshake.ts +7 -2
- package/src/working-repo.ts +30 -41
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');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentKind,
|
|
3
|
+
DroneRuntimeMetadata,
|
|
4
|
+
DroneRuntimeMetadataPatch,
|
|
5
|
+
} from 'borgmcp-shared/protocol';
|
|
6
|
+
import {
|
|
7
|
+
canonicalizeRepositoryIdentity,
|
|
8
|
+
validateReportedModel,
|
|
9
|
+
validateRuntimeMetadata,
|
|
10
|
+
validateRuntimeMetadataPatch,
|
|
11
|
+
} from 'borgmcp-shared/runtime-metadata';
|
|
12
|
+
import type { WorkingRepo } from './working-repo.js';
|
|
13
|
+
|
|
14
|
+
function safeReportedModel(value: string | null | undefined): string | null {
|
|
15
|
+
if (value == null) return null;
|
|
16
|
+
try {
|
|
17
|
+
return validateReportedModel(value);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function reportableRepository(repo: WorkingRepo | undefined) {
|
|
24
|
+
if (!repo || repo.state === 'unavailable' || repo.state === 'rejected') return null;
|
|
25
|
+
if (repo.name === null && repo.origin === null) {
|
|
26
|
+
return { working_repo_name: null, working_repo_origin: null };
|
|
27
|
+
}
|
|
28
|
+
if (repo.name === null || repo.origin === null) return null;
|
|
29
|
+
try {
|
|
30
|
+
const canonical = canonicalizeRepositoryIdentity(repo.origin, repo.name);
|
|
31
|
+
return {
|
|
32
|
+
working_repo_name: canonical.working_repo_name,
|
|
33
|
+
working_repo_origin: canonical.working_repo_origin,
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildRuntimeMetadataReport(input: {
|
|
41
|
+
agentKind: AgentKind | null | undefined;
|
|
42
|
+
reportedModel?: string | null;
|
|
43
|
+
workingRepo?: WorkingRepo;
|
|
44
|
+
}): DroneRuntimeMetadata {
|
|
45
|
+
const repository = reportableRepository(input.workingRepo);
|
|
46
|
+
return validateRuntimeMetadata({
|
|
47
|
+
agent_kind: input.agentKind ?? null,
|
|
48
|
+
reported_model: safeReportedModel(input.reportedModel),
|
|
49
|
+
working_repo_name: repository?.working_repo_name ?? null,
|
|
50
|
+
working_repo_origin: repository?.working_repo_origin ?? null,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildRuntimeMetadataPatch(input: {
|
|
55
|
+
agentKind: AgentKind | null;
|
|
56
|
+
reportedModel?: string;
|
|
57
|
+
workingRepo?: WorkingRepo;
|
|
58
|
+
}): DroneRuntimeMetadataPatch {
|
|
59
|
+
const patch: DroneRuntimeMetadataPatch = { agent_kind: input.agentKind };
|
|
60
|
+
if (input.reportedModel !== undefined) {
|
|
61
|
+
const model = safeReportedModel(input.reportedModel);
|
|
62
|
+
if (model !== null) patch.reported_model = model;
|
|
63
|
+
}
|
|
64
|
+
const repository = reportableRepository(input.workingRepo);
|
|
65
|
+
if (repository) Object.assign(patch, repository);
|
|
66
|
+
return validateRuntimeMetadataPatch(patch);
|
|
67
|
+
}
|
package/src/server-handshake.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
decodeProtocolTagPreflight,
|
|
16
16
|
ErrorCode,
|
|
17
17
|
type CreateCubeResponse,
|
|
18
|
+
type DroneRuntimeMetadata,
|
|
18
19
|
type ProtocolTagPreflight,
|
|
19
20
|
type ServerCapability,
|
|
20
21
|
} from 'borgmcp-shared/protocol';
|
|
@@ -235,6 +236,7 @@ export async function sendBorgServerAttach(
|
|
|
235
236
|
roleId: string;
|
|
236
237
|
operation: ServerSessionOperation;
|
|
237
238
|
priorDroneId?: string;
|
|
239
|
+
runtimeMetadata?: DroneRuntimeMetadata;
|
|
238
240
|
},
|
|
239
241
|
pendingBearer: string,
|
|
240
242
|
deps: {
|
|
@@ -274,9 +276,12 @@ export async function sendBorgServerAttach(
|
|
|
274
276
|
cube_id: request.cubeId,
|
|
275
277
|
role_id: request.roleId,
|
|
276
278
|
session_credential: pending.credential,
|
|
277
|
-
|
|
279
|
+
...(request.priorDroneId === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: { prior_drone_id: request.priorDroneId }),
|
|
282
|
+
...(request.runtimeMetadata === undefined
|
|
278
283
|
? {}
|
|
279
|
-
: {
|
|
284
|
+
: { runtime_metadata: request.runtimeMetadata }),
|
|
280
285
|
})),
|
|
281
286
|
});
|
|
282
287
|
} catch (error) {
|
package/src/working-repo.ts
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { canonicalizeRepositoryIdentity } from 'borgmcp-shared/runtime-metadata';
|
|
10
11
|
export interface WorkingRepo {
|
|
11
12
|
name: string | null;
|
|
12
|
-
/** Canonical
|
|
13
|
+
/** Canonical public HTTPS identity, never a raw Git remote URL. */
|
|
13
14
|
origin: string | null;
|
|
15
|
+
state?: 'known' | 'unknown' | 'unavailable' | 'rejected';
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface WorkingRepoDeps {
|
|
@@ -30,43 +32,19 @@ function trimmed(value: string | null | undefined): string | null {
|
|
|
30
32
|
return normalized ? normalized : null;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function nameFromIdentity(identity: string): string | null {
|
|
34
|
-
const lastPathSegment = identity.replace(/\/$/, '').split('/').pop();
|
|
35
|
-
const name = lastPathSegment?.replace(/\.git$/i, '').trim();
|
|
36
|
-
return name || null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
35
|
/**
|
|
40
|
-
* Convert a Git remote to
|
|
41
|
-
*
|
|
42
|
-
* URL userinfo, query strings, fragments, scheme, and SCP-style user prefixes
|
|
43
|
-
* are deliberately discarded. Inputs that cannot identify a host and path are
|
|
44
|
-
* treated as unreportable rather than forwarded verbatim.
|
|
36
|
+
* Convert a Git remote to the shared canonical public repository identity.
|
|
37
|
+
* Hostile or credential-bearing inputs are rejected rather than sanitized.
|
|
45
38
|
*/
|
|
46
|
-
export function canonicalizeWorkingRepoIdentity(origin: string):
|
|
47
|
-
const raw = origin.trim();
|
|
48
|
-
if (!raw) return null;
|
|
49
|
-
// Remote clients send this canonical form on subsequent lifecycle calls.
|
|
50
|
-
const canonical = raw.match(/^([A-Za-z0-9.-]+)\/([^?#\s]+)$/);
|
|
51
|
-
if (canonical) {
|
|
52
|
-
const host = canonical[1].toLowerCase();
|
|
53
|
-
const path = canonical[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
|
|
54
|
-
return host && path ? `${host}/${path}` : null;
|
|
55
|
-
}
|
|
39
|
+
export function canonicalizeWorkingRepoIdentity(origin: string): WorkingRepo | null {
|
|
56
40
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
41
|
+
const canonical = canonicalizeRepositoryIdentity(origin.trim());
|
|
42
|
+
return {
|
|
43
|
+
name: canonical.working_repo_name,
|
|
44
|
+
origin: canonical.working_repo_origin,
|
|
45
|
+
state: 'known',
|
|
46
|
+
};
|
|
61
47
|
} catch {
|
|
62
|
-
// SCP-style SSH remote: discard its optional user prefix and URL-like
|
|
63
|
-
// query/fragment suffix before accepting only host + repository path.
|
|
64
|
-
const match = raw.match(/^(?:[^@\s/:]+@)?([A-Za-z0-9.-]+):\/?([^?#\s]+)(?:[?#].*)?$/);
|
|
65
|
-
if (match) {
|
|
66
|
-
const host = match[1].toLowerCase();
|
|
67
|
-
const path = match[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
|
|
68
|
-
return host && path ? `${host}/${path}` : null;
|
|
69
|
-
}
|
|
70
48
|
return null;
|
|
71
49
|
}
|
|
72
50
|
}
|
|
@@ -83,17 +61,28 @@ export function resolveWorkingRepo(
|
|
|
83
61
|
deps: WorkingRepoDeps = {}
|
|
84
62
|
): WorkingRepo {
|
|
85
63
|
const runGit = deps.runGit ?? defaultRunGit;
|
|
86
|
-
|
|
64
|
+
let rootResult;
|
|
65
|
+
try {
|
|
66
|
+
rootResult = runGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
67
|
+
} catch {
|
|
68
|
+
return { name: null, origin: null, state: 'unavailable' };
|
|
69
|
+
}
|
|
87
70
|
const root = rootResult.status === 0 ? trimmed(rootResult.stdout) : null;
|
|
88
71
|
if (!root) {
|
|
89
|
-
return { name: null, origin: null };
|
|
72
|
+
return { name: null, origin: null, state: 'unknown' };
|
|
90
73
|
}
|
|
91
74
|
|
|
92
|
-
|
|
75
|
+
let originResult;
|
|
76
|
+
try {
|
|
77
|
+
originResult = runGit(cwd, ['config', '--get', 'remote.origin.url']);
|
|
78
|
+
} catch {
|
|
79
|
+
return { name: null, origin: null, state: 'unavailable' };
|
|
80
|
+
}
|
|
93
81
|
const originRaw = originResult.status === 0 ? trimmed(originResult.stdout) : null;
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
name:
|
|
97
|
-
origin,
|
|
82
|
+
if (!originRaw) return { name: null, origin: null, state: 'unknown' };
|
|
83
|
+
return canonicalizeWorkingRepoIdentity(originRaw) ?? {
|
|
84
|
+
name: null,
|
|
85
|
+
origin: null,
|
|
86
|
+
state: 'rejected',
|
|
98
87
|
};
|
|
99
88
|
}
|