borgmcp 2.7.3 → 2.9.0
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 +96 -230
- package/dist/assimilate-cmd.d.ts +1 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +18 -4
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +3 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +2 -3
- package/dist/cli-help.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/drone-lifecycle.d.ts +8 -0
- package/dist/drone-lifecycle.d.ts.map +1 -1
- package/dist/drone-lifecycle.js +19 -0
- package/dist/drone-lifecycle.js.map +1 -1
- package/dist/first-run-server.d.ts +10 -2
- package/dist/first-run-server.d.ts.map +1 -1
- package/dist/first-run-server.js +54 -7
- package/dist/first-run-server.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +4 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +28 -4
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts +2 -2
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +43 -8
- package/dist/remote-client.js.map +1 -1
- package/dist/runtime-metadata.d.ts +1 -1
- package/dist/runtime-metadata.d.ts.map +1 -1
- package/dist/runtime-metadata.js +3 -3
- package/dist/runtime-metadata.js.map +1 -1
- package/dist/server-errors.d.ts +9 -1
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +19 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-handshake.d.ts +5 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +18 -0
- package/dist/server-handshake.js.map +1 -1
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +10 -0
- package/dist/server-trust.js.map +1 -1
- package/dist/setup.js +5 -2
- package/dist/setup.js.map +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +8 -5
- package/dist/tool-manifest.js.map +1 -1
- package/dist/update-cmd.d.ts +1 -0
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +130 -19
- package/dist/update-cmd.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +7 -7
- package/docs/LOCAL_SERVER.md +3 -3
- package/docs/RELEASING.md +16 -6
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +25 -6
- package/src/assimilate-deps.ts +3 -0
- package/src/cli-help.ts +2 -3
- package/src/config.ts +4 -0
- package/src/drone-lifecycle.ts +25 -0
- package/src/first-run-server.ts +71 -8
- package/src/index.ts +15 -1
- package/src/log-stream.ts +26 -5
- package/src/remote-client.ts +58 -7
- package/src/runtime-metadata.ts +4 -3
- package/src/server-errors.ts +25 -0
- package/src/server-handshake.ts +30 -1
- package/src/server-trust.ts +11 -0
- package/src/setup.ts +5 -2
- package/src/tool-manifest.ts +8 -5
- package/src/update-cmd.ts +153 -19
package/src/assimilate-cmd.ts
CHANGED
|
@@ -277,7 +277,7 @@ export interface AssimilateDeps {
|
|
|
277
277
|
detectLocalServer: () => Promise<string | null>;
|
|
278
278
|
connectServer: (
|
|
279
279
|
apiUrl: string,
|
|
280
|
-
enrollment?: { invitation: string },
|
|
280
|
+
enrollment?: { invitation: string; confirmReplacement?: () => Promise<boolean> },
|
|
281
281
|
) => Promise<{
|
|
282
282
|
token: string;
|
|
283
283
|
trustIdentity: string;
|
|
@@ -361,6 +361,11 @@ function affirmative(answer: string): boolean {
|
|
|
361
361
|
return normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
function strictAffirmative(answer: string): boolean {
|
|
365
|
+
const normalized = answer.trim().toLowerCase();
|
|
366
|
+
return normalized === 'y' || normalized === 'yes';
|
|
367
|
+
}
|
|
368
|
+
|
|
364
369
|
async function selectAssimilationAuthority(
|
|
365
370
|
flags: AssimilateFlags,
|
|
366
371
|
deps: AssimilateDeps,
|
|
@@ -475,6 +480,14 @@ function reportServerFailure(
|
|
|
475
480
|
);
|
|
476
481
|
return 1;
|
|
477
482
|
}
|
|
483
|
+
if (error instanceof BorgServerError && error.code === 'LOCAL_CREDENTIAL_EXISTS') {
|
|
484
|
+
deps.stderr(
|
|
485
|
+
`A local enrollment for ${apiUrl} already exists and was not replaced. ` +
|
|
486
|
+
`Re-run ${localAssimilateCommand(apiUrl, true, mode)} and explicitly confirm replacement ` +
|
|
487
|
+
'only if the first enrolled client should be abandoned.\n',
|
|
488
|
+
);
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
478
491
|
if (error instanceof LegacySessionCredentialCollisionError) {
|
|
479
492
|
const recovery = mode === 'cube-init'
|
|
480
493
|
? localAssimilateCommand(error.origin, true, mode)
|
|
@@ -729,8 +742,14 @@ export async function runAssimilate(
|
|
|
729
742
|
);
|
|
730
743
|
return 1;
|
|
731
744
|
}
|
|
732
|
-
|
|
733
|
-
|
|
745
|
+
try {
|
|
746
|
+
serverAuth = await deps.connectServer(authority.apiUrl, {
|
|
747
|
+
invitation,
|
|
748
|
+
confirmReplacement: async () => strictAffirmative(await deps.prompt(
|
|
749
|
+
`A local enrollment for ${authority.apiUrl} already exists. Replacing it will orphan ` +
|
|
750
|
+
'the first enrolled client. Replace it? [y/N]: ',
|
|
751
|
+
)),
|
|
752
|
+
});
|
|
734
753
|
} finally {
|
|
735
754
|
// Strings cannot be zeroized in JavaScript, but drop this command's
|
|
736
755
|
// reference immediately after the exchange instead of retaining the
|
|
@@ -1492,7 +1511,7 @@ export async function runAssimilate(
|
|
|
1492
1511
|
deps.stderr(
|
|
1493
1512
|
'Borg could not enumerate this repository’s existing worktrees, so it did not risk creating a colliding sibling.\n' +
|
|
1494
1513
|
'Run `git worktree list` from this repository and resolve the reported Git error, then rerun `borg assimilate`.\n' +
|
|
1495
|
-
'A
|
|
1514
|
+
'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
|
|
1496
1515
|
);
|
|
1497
1516
|
return 1;
|
|
1498
1517
|
}
|
|
@@ -1565,13 +1584,13 @@ export async function runAssimilate(
|
|
|
1565
1584
|
? `Git left branch ${residualBranch} without a registered worktree; Borg preserved it.\n`
|
|
1566
1585
|
: '') +
|
|
1567
1586
|
'Run `git worktree list` and `git status` to inspect repository state, resolve the reported Git error, then rerun `borg assimilate`.\n' +
|
|
1568
|
-
'A
|
|
1587
|
+
'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
|
|
1569
1588
|
);
|
|
1570
1589
|
return 1;
|
|
1571
1590
|
}
|
|
1572
1591
|
deps.stderr(
|
|
1573
1592
|
`spawned sibling worktree at ${candidate} on branch ${wtBranch} (${startRef}); ` +
|
|
1574
|
-
`the original dir keeps its active
|
|
1593
|
+
`the original dir keeps its active drone binding — run \`borg reset-local-seat\` there if that binding is stale.\n`
|
|
1575
1594
|
);
|
|
1576
1595
|
deps.chdir(candidate);
|
|
1577
1596
|
deps.stderr(renderWorktreeSteeringNote(candidate, wtBranch, projectRoot));
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -241,6 +241,9 @@ export function buildDefaultAssimilateDeps(
|
|
|
241
241
|
connectServer: async (apiUrl, enrollment) => {
|
|
242
242
|
if (enrollment) {
|
|
243
243
|
return enrollLocalBorgServer(apiUrl, enrollment.invitation, {
|
|
244
|
+
...(enrollment.confirmReplacement === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: { confirmReplacement: enrollment.confirmReplacement }),
|
|
244
247
|
clientName: osHostname(),
|
|
245
248
|
});
|
|
246
249
|
}
|
package/src/cli-help.ts
CHANGED
|
@@ -74,9 +74,8 @@ export function clientSubcommandHelpText(
|
|
|
74
74
|
export function setupNextStepsText(): string {
|
|
75
75
|
return (
|
|
76
76
|
`◼ Next steps:\n` +
|
|
77
|
-
`1. Run \`borg server
|
|
78
|
-
`2.
|
|
79
|
-
`3. In a second terminal, cd into your project and run \`borg assimilate\`.\n`
|
|
77
|
+
`1. Run \`borg server start\` and leave that terminal open.\n` +
|
|
78
|
+
`2. In a second terminal, cd into your project's Git repository and run \`borg assimilate\`.\n`
|
|
80
79
|
);
|
|
81
80
|
}
|
|
82
81
|
|
package/src/config.ts
CHANGED
|
@@ -447,6 +447,7 @@ export async function activatePendingServerEnrollment(
|
|
|
447
447
|
credential: string;
|
|
448
448
|
clientId: string;
|
|
449
449
|
serverCapabilities: ServerCapability[];
|
|
450
|
+
allowReplacement?: boolean;
|
|
450
451
|
},
|
|
451
452
|
): Promise<void> {
|
|
452
453
|
validateServerCredentialBinding(input.origin, input.trustIdentity);
|
|
@@ -471,6 +472,9 @@ export async function activatePendingServerEnrollment(
|
|
|
471
472
|
} catch {
|
|
472
473
|
throw new Error('pending Borg server enrollment does not match the verified response');
|
|
473
474
|
}
|
|
475
|
+
if (!input.allowReplacement && await backend.get(serverCredentialAccount(input.origin, input.trustIdentity))) {
|
|
476
|
+
throw new Error('local Borg server enrollment already exists');
|
|
477
|
+
}
|
|
474
478
|
// Already inside the single store lock — use the UNLOCKED write body so we do
|
|
475
479
|
// not re-acquire (and self-deadlock on) CREDENTIALS_LOCK (CR3b).
|
|
476
480
|
await writeServerCredentialRecord(backend, {
|
package/src/drone-lifecycle.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export const DRONE_EVICTED_CODE = 'DRONE_EVICTED';
|
|
15
|
+
export const CUBE_DELETED_CODE = 'CUBE_DELETED';
|
|
15
16
|
|
|
16
17
|
export class DroneEvictedError extends Error {
|
|
17
18
|
constructor(
|
|
@@ -22,11 +23,19 @@ export class DroneEvictedError extends Error {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export class CubeDeletedError extends Error {
|
|
27
|
+
constructor(public readonly cubeName?: string) {
|
|
28
|
+
super('This cube was deleted.');
|
|
29
|
+
this.name = 'CubeDeletedError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* Marker the agent's /loop + role playbook branch on. Single-sourced so the
|
|
27
35
|
* SSE wake sentinel (log-stream) and the tool-result funnel (index) agree.
|
|
28
36
|
*/
|
|
29
37
|
export const EVICTED_RESULT_MARKER = '[CUBE-EVICTED]';
|
|
38
|
+
export const CUBE_DELETED_RESULT_MARKER = '[CUBE-DELETED]';
|
|
30
39
|
|
|
31
40
|
/**
|
|
32
41
|
* The recognizable tool RESULT the agent sees when an authed call returns the
|
|
@@ -43,6 +52,22 @@ export function formatEvictedToolResult(cubeName?: string): string {
|
|
|
43
52
|
);
|
|
44
53
|
}
|
|
45
54
|
|
|
55
|
+
export function formatCubeDeletedToolResult(cubeName?: string): string {
|
|
56
|
+
const cube = cubeName ?? 'the selected cube';
|
|
57
|
+
return (
|
|
58
|
+
`${CUBE_DELETED_RESULT_MARKER} Cube ${cube} was deleted.\n\n` +
|
|
59
|
+
'Borg has stopped listening for activity for this cube. Do not retry this request or restart the loop.\n\n' +
|
|
60
|
+
'Your worktree and project files are unchanged. Finish any local file safety checks, then end this agent session.'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatCubeDeletedErrorToolResult(
|
|
65
|
+
error: CubeDeletedError,
|
|
66
|
+
activeCubeName?: string,
|
|
67
|
+
): string {
|
|
68
|
+
return formatCubeDeletedToolResult(error.cubeName ?? activeCubeName);
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
/**
|
|
47
72
|
* Extract the structured error code from a worker error body. The worker error
|
|
48
73
|
* funnel (sanitizeError → createHttpError) emits `{ code, message }`; some
|
package/src/first-run-server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -14,10 +15,12 @@ const CLIENT_PACKAGE = 'borgmcp';
|
|
|
14
15
|
const SERVER_PACKAGE = 'borgmcp-server';
|
|
15
16
|
const SHARED_PACKAGE = 'borgmcp-shared';
|
|
16
17
|
const DEFAULT_CONNECT_COMMAND = 'borg assimilate --host <host>';
|
|
18
|
+
const CLIENT_ONBOARDING_ENV = 'BORG_CLIENT_ONBOARDING';
|
|
19
|
+
export const HINT_SUPPORTED_FROM = '0.8.1';
|
|
17
20
|
|
|
18
21
|
export type FirstRunServerInstallResult =
|
|
19
|
-
| { kind: 'present'; server: InstalledPackage }
|
|
20
|
-
| { kind: 'installed'; server: InstalledPackage }
|
|
22
|
+
| { kind: 'present'; server: InstalledPackage; suppressClientNextSteps?: boolean }
|
|
23
|
+
| { kind: 'installed'; server: InstalledPackage; suppressClientNextSteps?: boolean }
|
|
21
24
|
| { kind: 'declined' | 'non-interactive' | 'failed' };
|
|
22
25
|
|
|
23
26
|
export interface FirstRunServerInstallDeps {
|
|
@@ -32,7 +35,8 @@ export interface FirstRunServerInstallDeps {
|
|
|
32
35
|
version: string,
|
|
33
36
|
options?: { ignoreScripts?: boolean },
|
|
34
37
|
): Promise<void>;
|
|
35
|
-
|
|
38
|
+
runServerSetup(server: InstalledPackage, options?: { onboardingHint?: boolean }): Promise<number>;
|
|
39
|
+
confirm(message: string, defaultYes?: boolean): Promise<'yes' | 'no' | 'eof' | 'interrupted'>;
|
|
36
40
|
isTTY(): boolean;
|
|
37
41
|
stdout(text: string): void;
|
|
38
42
|
stderr(text: string): void;
|
|
@@ -84,6 +88,18 @@ export function buildDefaultFirstRunServerInstallDeps(): FirstRunServerInstallDe
|
|
|
84
88
|
publishedPackage: update.publishedPackage,
|
|
85
89
|
publishedVersions: update.publishedVersions,
|
|
86
90
|
installGlobal: update.installGlobal,
|
|
91
|
+
runServerSetup: (server, options = {}) => new Promise((resolve) => {
|
|
92
|
+
const childEnv = { ...process.env };
|
|
93
|
+
delete childEnv[CLIENT_ONBOARDING_ENV];
|
|
94
|
+
if (options.onboardingHint) childEnv[CLIENT_ONBOARDING_ENV] = '1';
|
|
95
|
+
const child = spawn(server.binPath, ['setup'], {
|
|
96
|
+
stdio: 'inherit',
|
|
97
|
+
shell: false,
|
|
98
|
+
env: childEnv,
|
|
99
|
+
});
|
|
100
|
+
child.once('error', () => resolve(1));
|
|
101
|
+
child.once('exit', (code) => resolve(code ?? 1));
|
|
102
|
+
}),
|
|
87
103
|
confirm: update.confirm,
|
|
88
104
|
isTTY: update.isTTY,
|
|
89
105
|
stdout: update.stdout,
|
|
@@ -100,6 +116,7 @@ export function buildDefaultFirstRunServerInstallDeps(): FirstRunServerInstallDe
|
|
|
100
116
|
export async function offerFirstRunServerInstall(
|
|
101
117
|
deps: FirstRunServerInstallDeps = buildDefaultFirstRunServerInstallDeps(),
|
|
102
118
|
connectCommand = DEFAULT_CONNECT_COMMAND,
|
|
119
|
+
options: { initializeServer?: boolean } = {},
|
|
103
120
|
): Promise<FirstRunServerInstallResult> {
|
|
104
121
|
let installed: InstalledPackage | null;
|
|
105
122
|
try {
|
|
@@ -112,7 +129,15 @@ export async function offerFirstRunServerInstall(
|
|
|
112
129
|
);
|
|
113
130
|
return { kind: 'failed' };
|
|
114
131
|
}
|
|
115
|
-
if (installed)
|
|
132
|
+
if (installed) {
|
|
133
|
+
return initializeServerIfRequested(
|
|
134
|
+
installed,
|
|
135
|
+
deps,
|
|
136
|
+
options.initializeServer,
|
|
137
|
+
'present',
|
|
138
|
+
shouldSuppressClientNextSteps(installed.version),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
116
141
|
|
|
117
142
|
if (!deps.isTTY()) {
|
|
118
143
|
deps.stderr(
|
|
@@ -140,9 +165,11 @@ export async function offerFirstRunServerInstall(
|
|
|
140
165
|
const command = exactInstallCommand(target.version);
|
|
141
166
|
const decision = await deps.confirm(
|
|
142
167
|
`No local ${SERVER_PACKAGE} installation was found. ` +
|
|
143
|
-
|
|
168
|
+
`Install ${SERVER_PACKAGE}@${target.version}, the local coordination server, ` +
|
|
169
|
+
`which uses ${SHARED_PACKAGE}@${target.sharedVersion}, now?\n` +
|
|
144
170
|
`Command: ${command}\n` +
|
|
145
|
-
|
|
171
|
+
`Install it now? [Y/n] `,
|
|
172
|
+
true,
|
|
146
173
|
);
|
|
147
174
|
if (decision !== 'yes') {
|
|
148
175
|
const reason = decision === 'interrupted'
|
|
@@ -172,7 +199,43 @@ export async function offerFirstRunServerInstall(
|
|
|
172
199
|
|
|
173
200
|
deps.stdout(
|
|
174
201
|
`Installed ${SERVER_PACKAGE}@${target.version} with ${SHARED_PACKAGE}@${target.sharedVersion}.\n` +
|
|
175
|
-
|
|
202
|
+
(options.initializeServer
|
|
203
|
+
? `The local server package is ready.\n`
|
|
204
|
+
: `Next, run \`borg server setup\`, then run \`borg server start\` in a terminal you keep open.\n`),
|
|
205
|
+
);
|
|
206
|
+
return initializeServerIfRequested(installed, deps, options.initializeServer, 'installed');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function initializeServerIfRequested(
|
|
210
|
+
server: InstalledPackage,
|
|
211
|
+
deps: FirstRunServerInstallDeps,
|
|
212
|
+
requested: boolean | undefined,
|
|
213
|
+
kind: 'present' | 'installed',
|
|
214
|
+
suppressClientNextSteps = false,
|
|
215
|
+
): Promise<FirstRunServerInstallResult> {
|
|
216
|
+
if (!requested) return { kind, server, ...(suppressClientNextSteps ? { suppressClientNextSteps: true } : {}) };
|
|
217
|
+
try {
|
|
218
|
+
const exitCode = await deps.runServerSetup(server, { onboardingHint: !suppressClientNextSteps });
|
|
219
|
+
if (exitCode === 0) {
|
|
220
|
+
return { kind, server, ...(suppressClientNextSteps ? { suppressClientNextSteps: true } : {}) };
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
deps.stderr(`Local server setup could not be completed. ${error instanceof Error ? error.message : String(error)}\n`);
|
|
224
|
+
}
|
|
225
|
+
deps.stderr(
|
|
226
|
+
'Run `borg server setup` to retry local server initialization, then run `borg server start`.\n',
|
|
176
227
|
);
|
|
177
|
-
return { kind: '
|
|
228
|
+
return { kind: 'failed' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function shouldSuppressClientNextSteps(version: unknown): boolean {
|
|
232
|
+
if (!isExactSemver(version)) return false;
|
|
233
|
+
const installedParts = version.split('.').slice(0, 3).map(Number);
|
|
234
|
+
const supportedParts = HINT_SUPPORTED_FROM.split('.').map(Number);
|
|
235
|
+
for (let index = 0; index < 3; index += 1) {
|
|
236
|
+
if (installedParts[index] !== supportedParts[index]) {
|
|
237
|
+
return installedParts[index] < supportedParts[index];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
178
241
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
21
21
|
|
|
22
22
|
import { assertRoleMatches } from './role-match.js';
|
|
23
|
+
import { CubeDeletionConfirmationError } from './server-errors.js';
|
|
23
24
|
|
|
24
25
|
import {
|
|
25
26
|
getCubeInfo,
|
|
@@ -103,7 +104,9 @@ import {
|
|
|
103
104
|
} from './roster-render.js';
|
|
104
105
|
import { resolveWorkingRepo } from './working-repo.js';
|
|
105
106
|
import {
|
|
107
|
+
CubeDeletedError,
|
|
106
108
|
DroneEvictedError,
|
|
109
|
+
formatCubeDeletedErrorToolResult,
|
|
107
110
|
formatEvictedToolResult,
|
|
108
111
|
} from './drone-lifecycle.js';
|
|
109
112
|
import {
|
|
@@ -1048,7 +1051,11 @@ export async function main() {
|
|
|
1048
1051
|
case 'borg_delete-cube': {
|
|
1049
1052
|
const cubeId = args?.cube_id as string;
|
|
1050
1053
|
if (!cubeId) throw new Error('cube_id is required');
|
|
1051
|
-
|
|
1054
|
+
const confirmCubeId = args?.confirm_cube_id as string | undefined;
|
|
1055
|
+
if (confirmCubeId !== cubeId) {
|
|
1056
|
+
throw new CubeDeletionConfirmationError(cubeId, confirmCubeId);
|
|
1057
|
+
}
|
|
1058
|
+
await deleteCube(cubeId, confirmCubeId);
|
|
1052
1059
|
return { content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }] };
|
|
1053
1060
|
}
|
|
1054
1061
|
|
|
@@ -1261,6 +1268,13 @@ export async function main() {
|
|
|
1261
1268
|
isError: true,
|
|
1262
1269
|
};
|
|
1263
1270
|
}
|
|
1271
|
+
if (error instanceof CubeDeletedError) {
|
|
1272
|
+
const active = await getActiveCube();
|
|
1273
|
+
return {
|
|
1274
|
+
content: [{ type: 'text', text: formatCubeDeletedErrorToolResult(error, active?.name) }],
|
|
1275
|
+
isError: true,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1264
1278
|
|
|
1265
1279
|
const localManageResult = formatLocalManageToolResult(error);
|
|
1266
1280
|
if (localManageResult) return localManageResult;
|
package/src/log-stream.ts
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
type LocalServerCursor,
|
|
41
41
|
} from './local-server-cursor.js';
|
|
42
42
|
import {
|
|
43
|
+
CubeDeletedError,
|
|
44
|
+
CUBE_DELETED_CODE,
|
|
43
45
|
DroneEvictedError,
|
|
44
46
|
DRONE_EVICTED_CODE,
|
|
45
47
|
EVICTED_RESULT_MARKER,
|
|
@@ -573,7 +575,7 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
|
|
|
573
575
|
// loop quiesces cleanly. The agent's graceful shutdown (TaskStop Monitor,
|
|
574
576
|
// no /loop reschedule) is driven separately by the EVICTED tool-result it
|
|
575
577
|
// already received on the authed call that produced this verdict.
|
|
576
|
-
if (err instanceof DroneEvictedError || err instanceof BorgServerError) {
|
|
578
|
+
if (err instanceof CubeDeletedError || err instanceof DroneEvictedError || err instanceof BorgServerError) {
|
|
577
579
|
if (active.localSessionCredentialRef) {
|
|
578
580
|
markSeatRejected(active.localSessionCredentialRef);
|
|
579
581
|
}
|
|
@@ -591,15 +593,15 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
|
|
|
591
593
|
streamState.ownership = await readOwnershipSnapshot(active.cubeId, active.droneId);
|
|
592
594
|
continue;
|
|
593
595
|
}
|
|
594
|
-
if (err instanceof DroneEvictedError) {
|
|
596
|
+
if (err instanceof DroneEvictedError || err instanceof CubeDeletedError) {
|
|
595
597
|
if (lease) await lease.release().catch(() => {});
|
|
596
598
|
lease = null;
|
|
597
599
|
leaseKey = null;
|
|
598
600
|
streamState.connected = false;
|
|
599
601
|
streamState.ownership = await readOwnershipSnapshot(active.cubeId, active.droneId);
|
|
600
|
-
process.stderr.write(
|
|
601
|
-
|
|
602
|
-
|
|
602
|
+
process.stderr.write(err instanceof CubeDeletedError
|
|
603
|
+
? '[borg-mcp log stream] cube deleted — stream terminated (no reconnect).\n'
|
|
604
|
+
: '[borg-mcp log stream] drone evicted — stream terminated (no reconnect).\n');
|
|
603
605
|
}
|
|
604
606
|
throw new TerminalStreamError();
|
|
605
607
|
}
|
|
@@ -939,6 +941,10 @@ export async function streamOnce(
|
|
|
939
941
|
if (active.localSessionCredentialRef) markSeatRejected(active.localSessionCredentialRef);
|
|
940
942
|
throw new DroneEvictedError();
|
|
941
943
|
}
|
|
944
|
+
if (code === CUBE_DELETED_CODE) {
|
|
945
|
+
if (active.localSessionCredentialRef) markSeatRejected(active.localSessionCredentialRef);
|
|
946
|
+
throw new CubeDeletedError();
|
|
947
|
+
}
|
|
942
948
|
// client#42: an expired resume cursor is RECOVERABLE, not terminal. The
|
|
943
949
|
// pointed-at entry was pruned server-side, so retrying the SAME cursor
|
|
944
950
|
// 410s forever (a wedged, silently-dead wake path). Reset the stream's
|
|
@@ -1010,6 +1016,13 @@ export async function streamOnce(
|
|
|
1010
1016
|
break;
|
|
1011
1017
|
}
|
|
1012
1018
|
|
|
1019
|
+
if (event.type === 'error') {
|
|
1020
|
+
if (active.localSessionCredentialRef) markSeatRejected(active.localSessionCredentialRef);
|
|
1021
|
+
if (event.code === CUBE_DELETED_CODE) throw new CubeDeletedError();
|
|
1022
|
+
if (event.code === DRONE_EVICTED_CODE) throw new DroneEvictedError();
|
|
1023
|
+
throw new BorgServerError('CREDENTIAL_REJECTED', 'Borg server terminated the stream');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1013
1026
|
if (event.type === 'heartbeat') {
|
|
1014
1027
|
streamState.lastHeartbeatAt = nowIso;
|
|
1015
1028
|
// First/baseline heartbeat absorb: until this session has seen
|
|
@@ -1198,6 +1211,7 @@ export type ParsedEvent =
|
|
|
1198
1211
|
| { type: 'bookmark'; as_of: string | null }
|
|
1199
1212
|
// gh#877 Path-A: terminal eviction control frame (wake hint, zero authority).
|
|
1200
1213
|
| { type: 'eviction'; id: string | null; cube_id: string | null; reason: string | null }
|
|
1214
|
+
| { type: 'error'; code: ErrorCode }
|
|
1201
1215
|
| { type: 'unknown'; raw: string };
|
|
1202
1216
|
|
|
1203
1217
|
/**
|
|
@@ -1357,6 +1371,13 @@ function parseEventBlock(block: string): ParsedEvent | null {
|
|
|
1357
1371
|
}
|
|
1358
1372
|
return { type: 'eviction', id: id || null, cube_id, reason };
|
|
1359
1373
|
}
|
|
1374
|
+
if (eventName === 'error') {
|
|
1375
|
+
try {
|
|
1376
|
+
return { type: 'error', code: decodeProtocolErrorEnvelope(JSON.parse(dataStr)).error.code };
|
|
1377
|
+
} catch {
|
|
1378
|
+
return { type: 'unknown', raw: block };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1360
1381
|
return { type: 'unknown', raw: block };
|
|
1361
1382
|
}
|
|
1362
1383
|
|
package/src/remote-client.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
17
|
import {
|
|
18
18
|
createProtocolEnvelope,
|
|
19
|
+
decodeDeleteCubeResponse,
|
|
19
20
|
decodeDroneRuntimeMetadataState,
|
|
20
21
|
decodeEvictDroneResult,
|
|
21
22
|
decodeProtocolEnvelope,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
decodeReassignDroneResult,
|
|
24
25
|
decodeUpdateDroneRuntimeMetadataResponse,
|
|
25
26
|
ErrorCode,
|
|
27
|
+
ProtocolContractError,
|
|
26
28
|
type AgentKind,
|
|
27
29
|
type EvictDroneResult,
|
|
28
30
|
type ReassignDroneResult,
|
|
@@ -30,7 +32,12 @@ import {
|
|
|
30
32
|
import { consolePrefix } from './console-prefix.js';
|
|
31
33
|
import { debugLog } from './debug.js';
|
|
32
34
|
import { assertUuidShape } from './evict-drone.js';
|
|
33
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
CubeDeletedError,
|
|
37
|
+
CUBE_DELETED_CODE,
|
|
38
|
+
DroneEvictedError,
|
|
39
|
+
DRONE_EVICTED_CODE,
|
|
40
|
+
} from './drone-lifecycle.js';
|
|
34
41
|
import type { MessageTaxonomy, MessageTaxonomyClass } from 'borgmcp-shared/templates';
|
|
35
42
|
import { getTemplate, type Template, type TemplateRole } from 'borgmcp-shared/templates';
|
|
36
43
|
import { parseRoleSections } from 'borgmcp-shared/role-section';
|
|
@@ -41,8 +48,10 @@ import { loadBorgServerTrust, type ServerFetch } from './server-trust.js';
|
|
|
41
48
|
import {
|
|
42
49
|
BorgServerError,
|
|
43
50
|
BorgServerHttpError,
|
|
51
|
+
BorgProtocolMismatchError,
|
|
44
52
|
BorgServerTrustError,
|
|
45
53
|
BorgServerUnreachableError,
|
|
54
|
+
CubeDeletionConfirmationError,
|
|
46
55
|
LocalManageCredentialUnavailableError,
|
|
47
56
|
LocalManageRequiredError,
|
|
48
57
|
} from './server-errors.js';
|
|
@@ -226,6 +235,12 @@ async function decodeLocalProtocolResponse<T>(
|
|
|
226
235
|
// CR5: a TYPED transport-timeout verdict (message kept for call-site parity).
|
|
227
236
|
throw new BorgServerUnreachableError('Local Borg server request timed out');
|
|
228
237
|
}
|
|
238
|
+
if (
|
|
239
|
+
error instanceof ProtocolContractError &&
|
|
240
|
+
error.code === ErrorCode.UNSUPPORTED_PROTOCOL_VERSION
|
|
241
|
+
) {
|
|
242
|
+
throw new BorgProtocolMismatchError();
|
|
243
|
+
}
|
|
229
244
|
throw error;
|
|
230
245
|
} finally {
|
|
231
246
|
clearTimeout(timeout);
|
|
@@ -329,6 +344,9 @@ async function localManageRequest<T>(
|
|
|
329
344
|
}),
|
|
330
345
|
}), true, decodePayload);
|
|
331
346
|
} catch (error) {
|
|
347
|
+
if (error instanceof CubeDeletedError) {
|
|
348
|
+
throw new CubeDeletedError(operation.cubeName);
|
|
349
|
+
}
|
|
332
350
|
if (
|
|
333
351
|
error instanceof BorgServerHttpError &&
|
|
334
352
|
error.status === 403 &&
|
|
@@ -704,6 +722,7 @@ async function authedFetch(
|
|
|
704
722
|
// terminal controls. Decode only the bounded protocol error code for typed
|
|
705
723
|
// branching; never surface the server-provided message or details.
|
|
706
724
|
let code: ErrorCode | undefined;
|
|
725
|
+
let protocolMismatch = false;
|
|
707
726
|
try {
|
|
708
727
|
const body = await readBoundedResponseBody(
|
|
709
728
|
response,
|
|
@@ -713,7 +732,13 @@ async function authedFetch(
|
|
|
713
732
|
const parsed = JSON.parse(body);
|
|
714
733
|
try {
|
|
715
734
|
code = decodeProtocolErrorEnvelope(parsed).error.code;
|
|
716
|
-
} catch {
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (
|
|
737
|
+
error instanceof ProtocolContractError &&
|
|
738
|
+
error.code === ErrorCode.UNSUPPORTED_PROTOCOL_VERSION
|
|
739
|
+
) {
|
|
740
|
+
protocolMismatch = true;
|
|
741
|
+
}
|
|
717
742
|
if (
|
|
718
743
|
parsed !== null && typeof parsed === 'object' &&
|
|
719
744
|
parsed.error !== null && typeof parsed.error === 'object' &&
|
|
@@ -738,10 +763,15 @@ async function authedFetch(
|
|
|
738
763
|
code = undefined;
|
|
739
764
|
}
|
|
740
765
|
debugLog(`✗ ${response.status} ${method} ${path}`);
|
|
766
|
+
if (protocolMismatch) throw new BorgProtocolMismatchError();
|
|
741
767
|
if (droneSession !== undefined && response.status === 410 && code === DRONE_EVICTED_CODE) {
|
|
742
768
|
if (localSessionCredentialRef !== undefined) markSeatRejected(localSessionCredentialRef);
|
|
743
769
|
throw new DroneEvictedError();
|
|
744
770
|
}
|
|
771
|
+
if (response.status === 410 && code === CUBE_DELETED_CODE) {
|
|
772
|
+
if (localSessionCredentialRef !== undefined) markSeatRejected(localSessionCredentialRef);
|
|
773
|
+
throw new CubeDeletedError();
|
|
774
|
+
}
|
|
745
775
|
throw new BorgServerHttpError(
|
|
746
776
|
response.status,
|
|
747
777
|
`Borg server request failed (HTTP ${response.status})`,
|
|
@@ -1011,7 +1041,7 @@ export async function regen(
|
|
|
1011
1041
|
opts: {
|
|
1012
1042
|
since?: string;
|
|
1013
1043
|
/** Advisory self-report from the running agent; never model-routing config. */
|
|
1014
|
-
reportedModel?: string;
|
|
1044
|
+
reportedModel?: string | null;
|
|
1015
1045
|
/** Positively identified running Agent CLI; null means explicitly unknown. */
|
|
1016
1046
|
agentKind?: AgentKind | null;
|
|
1017
1047
|
/** Current cwd-derived identity; refreshed each regen to avoid stale routing data. */
|
|
@@ -1047,7 +1077,7 @@ export async function regen(
|
|
|
1047
1077
|
) {
|
|
1048
1078
|
const patch = buildRuntimeMetadataPatch({
|
|
1049
1079
|
agentKind: opts.agentKind ?? null,
|
|
1050
|
-
reportedModel: opts.reportedModel,
|
|
1080
|
+
reportedModel: opts.reportedModel ?? null,
|
|
1051
1081
|
workingRepo: opts.workingRepo,
|
|
1052
1082
|
});
|
|
1053
1083
|
try {
|
|
@@ -1316,9 +1346,30 @@ export async function patchTaxonomyClass(
|
|
|
1316
1346
|
* Delete a cube. Cascade-deletes all roles, drones, and log entries.
|
|
1317
1347
|
* Requires a live cube-manage grant on the selected local client.
|
|
1318
1348
|
*/
|
|
1319
|
-
export async function deleteCube(cubeId: string): Promise<void> {
|
|
1320
|
-
|
|
1321
|
-
|
|
1349
|
+
export async function deleteCube(cubeId: string, confirmCubeId: string): Promise<void> {
|
|
1350
|
+
if (confirmCubeId !== cubeId) {
|
|
1351
|
+
throw new CubeDeletionConfirmationError(cubeId, confirmCubeId);
|
|
1352
|
+
}
|
|
1353
|
+
assertUuidShape(cubeId, 'cube_id');
|
|
1354
|
+
const active = await getActiveCube();
|
|
1355
|
+
if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
1356
|
+
const cubeName = cubeId === active.cubeId ? active.name : cubeId;
|
|
1357
|
+
const result = await localManageRequest(
|
|
1358
|
+
active,
|
|
1359
|
+
`/api/cubes/${cubeId}`,
|
|
1360
|
+
'DELETE',
|
|
1361
|
+
{
|
|
1362
|
+
operation: `delete cube ${manageCopyValue(cubeName)}`,
|
|
1363
|
+
cubeName,
|
|
1364
|
+
noMutation: 'The cube was not deleted.',
|
|
1365
|
+
},
|
|
1366
|
+
{},
|
|
1367
|
+
decodeDeleteCubeResponse,
|
|
1368
|
+
);
|
|
1369
|
+
if (!result) throw new Error('Local Borg server returned an empty cube deletion response');
|
|
1370
|
+
if (result.cube_id !== cubeId) {
|
|
1371
|
+
throw new Error('Local Borg server returned a deletion response for an unexpected cube');
|
|
1372
|
+
}
|
|
1322
1373
|
}
|
|
1323
1374
|
|
|
1324
1375
|
/**
|
package/src/runtime-metadata.ts
CHANGED
|
@@ -53,13 +53,14 @@ export function buildRuntimeMetadataReport(input: {
|
|
|
53
53
|
|
|
54
54
|
export function buildRuntimeMetadataPatch(input: {
|
|
55
55
|
agentKind: AgentKind | null;
|
|
56
|
-
reportedModel?: string;
|
|
56
|
+
reportedModel?: string | null;
|
|
57
57
|
workingRepo?: WorkingRepo;
|
|
58
58
|
}): DroneRuntimeMetadataPatch {
|
|
59
59
|
const patch: DroneRuntimeMetadataPatch = { agent_kind: input.agentKind };
|
|
60
60
|
if (input.reportedModel !== undefined) {
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
patch.reported_model = input.reportedModel === null
|
|
62
|
+
? null
|
|
63
|
+
: validateReportedModel(input.reportedModel);
|
|
63
64
|
}
|
|
64
65
|
const repository = reportableRepository(input.workingRepo);
|
|
65
66
|
if (repository) Object.assign(patch, repository);
|
package/src/server-errors.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type BorgServerErrorCode =
|
|
|
2
2
|
| 'NOT_ENROLLED'
|
|
3
3
|
| 'CREDENTIAL_REJECTED'
|
|
4
4
|
| 'INVITATION_REJECTED'
|
|
5
|
+
| 'LOCAL_CREDENTIAL_EXISTS'
|
|
5
6
|
| 'CREATE_CUBE_DENIED'
|
|
6
7
|
| 'ATTACH_CONFLICT'
|
|
7
8
|
| 'SESSION_REJECTED'
|
|
@@ -35,6 +36,16 @@ export class BorgServerHttpError extends Error {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
export class BorgProtocolMismatchError extends Error {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(
|
|
42
|
+
'This client and the selected Borg server use different protocol versions. ' +
|
|
43
|
+
'Update `borgmcp-server` and `borgmcp` to matching releases, server first and then client.',
|
|
44
|
+
);
|
|
45
|
+
this.name = 'BorgProtocolMismatchError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export class LocalManageRequiredError extends Error {
|
|
39
50
|
constructor(
|
|
40
51
|
public readonly operation: string,
|
|
@@ -106,6 +117,20 @@ export class CubeCreationConfirmationError extends Error {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
export class CubeDeletionConfirmationError extends Error {
|
|
121
|
+
constructor(
|
|
122
|
+
public readonly cubeId: string,
|
|
123
|
+
public readonly confirmCubeId: string | undefined,
|
|
124
|
+
) {
|
|
125
|
+
const supplied = confirmCubeId === undefined ? '(missing)' : `"${confirmCubeId}"`;
|
|
126
|
+
super(
|
|
127
|
+
`Cube deletion is irreversible. The confirmation cube ID ${supplied} must exactly match ` +
|
|
128
|
+
`the requested cube ID "${cubeId}". No cube was deleted.`,
|
|
129
|
+
);
|
|
130
|
+
this.name = 'CubeDeletionConfirmationError';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
export type RepositoryAssociationFailure =
|
|
110
135
|
| 'repository-already-associated'
|
|
111
136
|
| 'cube-already-associated'
|