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.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/dist/agent-runtime.d.ts +2 -0
  3. package/dist/agent-runtime.d.ts.map +1 -1
  4. package/dist/agent-runtime.js +5 -1
  5. package/dist/agent-runtime.js.map +1 -1
  6. package/dist/assimilate-cmd.d.ts +2 -0
  7. package/dist/assimilate-cmd.d.ts.map +1 -1
  8. package/dist/assimilate-cmd.js +2 -0
  9. package/dist/assimilate-cmd.js.map +1 -1
  10. package/dist/assimilate-deps.d.ts.map +1 -1
  11. package/dist/assimilate-deps.js +6 -0
  12. package/dist/assimilate-deps.js.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +9 -9
  15. package/dist/index.js.map +1 -1
  16. package/dist/regen-format.d.ts.map +1 -1
  17. package/dist/regen-format.js +6 -4
  18. package/dist/regen-format.js.map +1 -1
  19. package/dist/regen.js +2 -0
  20. package/dist/regen.js.map +1 -1
  21. package/dist/remote-client.d.ts +10 -1
  22. package/dist/remote-client.d.ts.map +1 -1
  23. package/dist/remote-client.js +45 -5
  24. package/dist/remote-client.js.map +1 -1
  25. package/dist/roster-render.d.ts +11 -0
  26. package/dist/roster-render.d.ts.map +1 -1
  27. package/dist/roster-render.js +41 -8
  28. package/dist/roster-render.js.map +1 -1
  29. package/dist/runtime-metadata.d.ts +13 -0
  30. package/dist/runtime-metadata.d.ts.map +1 -0
  31. package/dist/runtime-metadata.js +52 -0
  32. package/dist/runtime-metadata.js.map +1 -0
  33. package/dist/server-handshake.d.ts +2 -1
  34. package/dist/server-handshake.d.ts.map +1 -1
  35. package/dist/server-handshake.js +3 -0
  36. package/dist/server-handshake.js.map +1 -1
  37. package/dist/working-repo.d.ts +5 -7
  38. package/dist/working-repo.d.ts.map +1 -1
  39. package/dist/working-repo.js +30 -40
  40. package/dist/working-repo.js.map +1 -1
  41. package/docs/EXTRACTION_PROVENANCE.md +7 -7
  42. package/docs/LOCAL_SERVER.md +1 -1
  43. package/docs/RELEASING.md +11 -2
  44. package/package.json +2 -2
  45. package/src/agent-runtime.ts +8 -1
  46. package/src/assimilate-cmd.ts +3 -1
  47. package/src/assimilate-deps.ts +10 -4
  48. package/src/index.ts +17 -13
  49. package/src/regen-format.ts +12 -5
  50. package/src/regen.ts +2 -0
  51. package/src/remote-client.ts +60 -5
  52. package/src/roster-render.ts +58 -8
  53. package/src/runtime-metadata.ts +67 -0
  54. package/src/server-handshake.ts +7 -2
  55. package/src/working-repo.ts +30 -41
@@ -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} (${roleLabel}) — last seen ${lastSeen} · ${marker}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}${reportedModelMarker}${workingRepoMarker}`
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} (${roleLabel}) — last seen ${lastSeen}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}${reportedModelMarker}${workingRepoMarker}`
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
+ }
@@ -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
- ...(request.priorDroneId === undefined
279
+ ...(request.priorDroneId === undefined
280
+ ? {}
281
+ : { prior_drone_id: request.priorDroneId }),
282
+ ...(request.runtimeMetadata === undefined
278
283
  ? {}
279
- : { prior_drone_id: request.priorDroneId }),
284
+ : { runtime_metadata: request.runtimeMetadata }),
280
285
  })),
281
286
  });
282
287
  } catch (error) {
@@ -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 host/path identity, never a raw Git remote URL. */
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 a non-secret `host/org/repo` identity.
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): string | null {
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 url = new URL(origin);
58
- if (!['http:', 'https:', 'ssh:', 'git:'].includes(url.protocol)) return null;
59
- const path = url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
60
- return url.hostname && path ? `${url.hostname.toLowerCase()}/${path}` : null;
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
- const rootResult = runGit(cwd, ['rev-parse', '--show-toplevel']);
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
- const originResult = runGit(cwd, ['config', '--get', 'remote.origin.url']);
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
- const origin = originRaw ? canonicalizeWorkingRepoIdentity(originRaw) : null;
95
- return {
96
- name: origin ? nameFromIdentity(origin) : null,
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
  }