borgmcp 2.9.0 → 2.10.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.
Files changed (87) hide show
  1. package/dist/assimilate-cmd.d.ts +13 -0
  2. package/dist/assimilate-cmd.d.ts.map +1 -1
  3. package/dist/assimilate-cmd.js +169 -10
  4. package/dist/assimilate-cmd.js.map +1 -1
  5. package/dist/assimilate-deps.d.ts.map +1 -1
  6. package/dist/assimilate-deps.js +26 -1
  7. package/dist/assimilate-deps.js.map +1 -1
  8. package/dist/assimilate-welcome.d.ts.map +1 -1
  9. package/dist/assimilate-welcome.js +1 -0
  10. package/dist/assimilate-welcome.js.map +1 -1
  11. package/dist/claude.d.ts.map +1 -1
  12. package/dist/claude.js +27 -0
  13. package/dist/claude.js.map +1 -1
  14. package/dist/cli-help.d.ts +1 -0
  15. package/dist/cli-help.d.ts.map +1 -1
  16. package/dist/cli-help.js +11 -0
  17. package/dist/cli-help.js.map +1 -1
  18. package/dist/config.d.ts +11 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +169 -13
  21. package/dist/config.js.map +1 -1
  22. package/dist/invitation-artifact.d.ts +33 -0
  23. package/dist/invitation-artifact.d.ts.map +1 -0
  24. package/dist/invitation-artifact.js +89 -0
  25. package/dist/invitation-artifact.js.map +1 -0
  26. package/dist/launch-gate.d.ts +5 -4
  27. package/dist/launch-gate.d.ts.map +1 -1
  28. package/dist/launch-gate.js +7 -8
  29. package/dist/launch-gate.js.map +1 -1
  30. package/dist/parse-assimilate-args.d.ts.map +1 -1
  31. package/dist/parse-assimilate-args.js +9 -3
  32. package/dist/parse-assimilate-args.js.map +1 -1
  33. package/dist/recover-enrollment-cmd.d.ts +17 -0
  34. package/dist/recover-enrollment-cmd.d.ts.map +1 -0
  35. package/dist/recover-enrollment-cmd.js +62 -0
  36. package/dist/recover-enrollment-cmd.js.map +1 -0
  37. package/dist/regen-format.d.ts +9 -0
  38. package/dist/regen-format.d.ts.map +1 -1
  39. package/dist/regen-format.js +15 -0
  40. package/dist/regen-format.js.map +1 -1
  41. package/dist/regen.js +27 -3
  42. package/dist/regen.js.map +1 -1
  43. package/dist/seat-store.d.ts.map +1 -1
  44. package/dist/seat-store.js +7 -0
  45. package/dist/seat-store.js.map +1 -1
  46. package/dist/server-facade.d.ts +1 -1
  47. package/dist/server-facade.d.ts.map +1 -1
  48. package/dist/server-facade.js +2 -2
  49. package/dist/server-facade.js.map +1 -1
  50. package/dist/server-handshake.d.ts +19 -3
  51. package/dist/server-handshake.d.ts.map +1 -1
  52. package/dist/server-handshake.js +86 -9
  53. package/dist/server-handshake.js.map +1 -1
  54. package/dist/server-trust.d.ts +8 -0
  55. package/dist/server-trust.d.ts.map +1 -1
  56. package/dist/server-trust.js +165 -5
  57. package/dist/server-trust.js.map +1 -1
  58. package/dist/token-store.d.ts +3 -0
  59. package/dist/token-store.d.ts.map +1 -1
  60. package/dist/token-store.js +2 -0
  61. package/dist/token-store.js.map +1 -1
  62. package/dist/unknown-subcommand.d.ts +1 -1
  63. package/dist/unknown-subcommand.d.ts.map +1 -1
  64. package/dist/unknown-subcommand.js +1 -0
  65. package/dist/unknown-subcommand.js.map +1 -1
  66. package/docs/EXTRACTION_PROVENANCE.md +11 -7
  67. package/docs/LOCAL_SERVER.md +12 -3
  68. package/docs/RELEASING.md +12 -6
  69. package/package.json +2 -2
  70. package/src/assimilate-cmd.ts +219 -24
  71. package/src/assimilate-deps.ts +26 -0
  72. package/src/assimilate-welcome.ts +1 -0
  73. package/src/claude.ts +22 -0
  74. package/src/cli-help.ts +14 -0
  75. package/src/config.ts +165 -10
  76. package/src/invitation-artifact.ts +116 -0
  77. package/src/launch-gate.ts +7 -8
  78. package/src/parse-assimilate-args.ts +11 -4
  79. package/src/recover-enrollment-cmd.ts +63 -0
  80. package/src/regen-format.ts +25 -0
  81. package/src/regen.ts +30 -2
  82. package/src/seat-store.ts +6 -0
  83. package/src/server-facade.ts +2 -2
  84. package/src/server-handshake.ts +113 -6
  85. package/src/server-trust.ts +180 -5
  86. package/src/token-store.ts +5 -0
  87. package/src/unknown-subcommand.ts +1 -0
@@ -11,7 +11,7 @@
11
11
  * (`--cli`, `--remote`, agent passthrough, …) still fall through to launch.
12
12
  */
13
13
  /** The subcommands the claude.ts router dispatches on (lines 107-176). */
14
- export declare const KNOWN_SUBCOMMANDS: readonly ["setup", "update", "assimilate", "reset-local-seat", "spawn", "sync", "cleanup", "launch-all", "server"];
14
+ export declare const KNOWN_SUBCOMMANDS: readonly ["setup", "update", "assimilate", "reset-local-seat", "recover-enrollment", "spawn", "sync", "cleanup", "launch-all", "server"];
15
15
  /**
16
16
  * Returns the offending command string when argv[2] is an unknown non-flag
17
17
  * positional (caller should error + exit 1), or null when it's bare `borg`,
@@ -1 +1 @@
1
- {"version":3,"file":"unknown-subcommand.d.ts","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,oHAUpB,CAAC;AAEX;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAK1E"}
1
+ {"version":3,"file":"unknown-subcommand.d.ts","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,0IAWpB,CAAC;AAEX;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAK1E"}
@@ -16,6 +16,7 @@ export const KNOWN_SUBCOMMANDS = [
16
16
  'update',
17
17
  'assimilate',
18
18
  'reset-local-seat',
19
+ 'recover-enrollment',
19
20
  'spawn',
20
21
  'sync',
21
22
  'cleanup',
@@ -1 +1 @@
1
- {"version":3,"file":"unknown-subcommand.js","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,kBAAkB;IAClB,OAAO;IACP,MAAM;IACN,SAAS;IACT,YAAY;IACZ,QAAQ;CACA,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAyB;IACzD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,cAAc;IACpD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,8BAA8B;IACtE,IAAK,iBAAuC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,OAAO,KAAK,CAAC,CAAC,wCAAwC;AACxD,CAAC"}
1
+ {"version":3,"file":"unknown-subcommand.js","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,kBAAkB;IAClB,oBAAoB;IACpB,OAAO;IACP,MAAM;IACN,SAAS;IACT,YAAY;IACZ,QAAQ;CACA,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAyB;IACzD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,cAAc;IACpD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,8BAA8B;IACtE,IAAK,iBAAuC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,OAAO,KAAK,CAAC,CAAC,wCAAwC;AACxD,CAAC"}
@@ -22,7 +22,7 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
22
22
  recovery, `2.0.2`, `2.0.3`, `2.0.4`, `2.0.5`, and `2.0.6` successors were
23
23
  published and registry-verified. The immutable `2.0.7` workflow failed before
24
24
  package creation or npm publication. The `2.0.8`, `2.0.9`, `2.0.10`, `2.0.11`,
25
- `2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, so the next candidate identity is `2.9.0`. Extraction and
25
+ `2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, and `2.9.0` was subsequently published, so the next candidate identity is `2.10.0`. Extraction and
26
26
  versioning do not authorize publication.
27
27
 
28
28
  ## Review Holds
@@ -38,11 +38,15 @@ artifact reaches no hosted authority.
38
38
  Local enrollment now uses the reviewed client-generated credential/retry
39
39
  contract, with a pre-request `PENDING` record in the local 0600-permission seat
40
40
  store, exact-tuple ambiguous retry, and verified activation. The contract now
41
- resolves to the audited registry `borgmcp-shared@0.7.1` with integrity
42
- `sha512-d4GE7ezGyoFN0uwA62gvRPUMfMdJ1kxhTunqU8dW3Qhy1n2SlNXZWr1Nthm6Goo0bAgvax8KEywyReodA6FyQA==`.
43
- The matching `borgmcp-server@0.8.0` release is published with integrity
44
- `sha512-OSnBBcdZOM88Dokr7WICcTmF19c+Nr0TOl7NXy8DSUeCpBttiKKx+KuFvGbaXAWikFkm0UUBUca5cLZAOWw4Xw==`
45
- and pins the same `borgmcp-shared@0.7.1` contract. Client `borgmcp@2.8.0` is published. The immutable `v2.0.7`
41
+ resolves to the audited registry `borgmcp-shared@0.8.0` with integrity
42
+ `sha512-0p/ZC03Y6G4N6bfBjMD5b+WcQXcLalhU/3QqGjSWYCFKa3q2/183O53w6Tu6y8h2DkhTw5prnamsOozmVpXZew==`.
43
+ The coupled `borgmcp-server@0.9.0` release was published on 2026-08-02 for the
44
+ client `borgmcp@2.10.0` line. Its annotated tag object
45
+ `7021a2e1f551aa0c1716c9e3f29f7b8bc4b0ccb0` peels to protected-main commit
46
+ `734051375d77013f8fd5b396af12feb53d5af96d`; the same-run artifact integrity is
47
+ `sha512-lOxIPg3WcjSBte46iTM7SAFVN6Y0b2oizOKAJ9Q1EijBdOlmsh/cQlpUIaa8F2UJPaTDFsXy3M64gpGs6XgKzA==`
48
+ in the `borgmcp-server-0.9.0-release` registry decision. Both releases pin the
49
+ audited `borgmcp-shared@0.8.0` dependency above. Client `borgmcp@2.9.0` is published. The immutable `v2.0.7`
46
50
  attempt failed before publication and remains preserved. Publication of the next
47
- candidate remains gated by reviewed `v2.9.0` source, a fresh annotated tag, and
51
+ candidate remains gated by reviewed `v2.10.0` source, a fresh annotated tag, and
48
52
  the exact-artifact and protected-publication gates.
@@ -140,6 +140,14 @@ create distinct bounded cubes. An ordinary enrolled client is denied before a
140
140
  create request is sent. Cloud-only capabilities fail explicitly rather than
141
141
  being redirected.
142
142
 
143
+ ## Agent Launch State
144
+
145
+ Borg tools are inactive unless the agent session was launched with `borg`.
146
+ For Claude Code and Codex, `BORG_DISABLE_LAUNCH_REMINDER` is a presence-based
147
+ local opt-out for launch orientation messaging; setting it does not activate
148
+ Borg tools or change server authorization. OpenCode has no launch-reminder
149
+ surface.
150
+
143
151
  ## Updating Borg
144
152
 
145
153
  For an npm-global installation, use the whole-product command:
@@ -227,9 +235,10 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
227
235
 
228
236
  ## Release status
229
237
 
230
- This self-hosted path pins published `borgmcp-shared@0.7.1` and requires the
231
- matching published `borgmcp-server@0.8.0` for protocol v7 repository resolve,
232
- association, and cube deletion. The server owner-enrollment, repository resolve/association,
238
+ This client candidate pins published `borgmcp-shared@0.8.0` and requires the
239
+ matching coupled client/server release for protocol v7 repository resolve,
240
+ association, and cube deletion. The already-published server 0.8.0 retains its
241
+ immutable older shared dependency pin and is not that coupled artifact. The server owner-enrollment, repository resolve/association,
233
242
  cube-create, attach, restart, log, and SSE implementation must also pass the full
234
243
  process-level local dogfood gate. Until that gate opens the self-hosted path remains preview-only, and the client
235
244
  publish is deferred accordingly.
package/docs/RELEASING.md CHANGED
@@ -178,8 +178,13 @@ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.8.0
178
178
  `ad40e93ee9b1ff2801119c1c763ad526974ae2a6`. Workflow run `30639790368`, attempt 1,
179
179
  successfully published that exact source as `borgmcp@2.8.0`; the same-run artifact report records integrity
180
180
  `sha512-hKNyWwVg/8WJ/qcqaNjZVylMw2UzgFEhE7sAJmWy/BjNU4lEa6ox8N7SdbEt8hhKsySMxt+YTFbNFitRtf9u3g==`.
181
+ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.9.0` tag object
182
+ `26df119da33e914f857f820e68df6e887afed949` peels to protected-main commit
183
+ `895f12065c09ec5c0a35f770f26ec89f36f2feaa`. Workflow run `30695501563`, attempt 1,
184
+ successfully published that exact source as `borgmcp@2.9.0`; the same-run artifact report records integrity
185
+ `sha512-e10KK//zAP6xRDtUjCrLqzVyeX5zd/39JD4LksBhNVL0ZIGthu5ZLahPMrcCjGl6lZQHNTCo8Tez5c1jmThgGQ==`.
181
186
  Never move, replace, reuse, or rerun that tag or workflow. The next candidate
182
- uses the unused `v2.9.0` identity from a fresh reviewed protected-main commit
187
+ uses the unused `v2.10.0` identity from a fresh reviewed protected-main commit
183
188
  and requires the complete release gate again.
184
189
 
185
190
  ## Release Prerequisites
@@ -191,12 +196,13 @@ Before creating the release tag, independently verify all of these conditions:
191
196
  - the extraction review confirms no private backend secrets, deployment
192
197
  configuration, customer data, local state, or duplicated shared contracts
193
198
  entered the public package;
194
- - the exact audited registry dependency `borgmcp-shared@0.7.1` remains locked to
199
+ - the exact audited registry dependency `borgmcp-shared@0.8.0` remains locked to
195
200
  its canonical tarball and integrity
196
- `sha512-d4GE7ezGyoFN0uwA62gvRPUMfMdJ1kxhTunqU8dW3Qhy1n2SlNXZWr1Nthm6Goo0bAgvax8KEywyReodA6FyQA==`;
197
- - the client and matching published `borgmcp-server@0.8.0`, integrity
198
- `sha512-OSnBBcdZOM88Dokr7WICcTmF19c+Nr0TOl7NXy8DSUeCpBttiKKx+KuFvGbaXAWikFkm0UUBUca5cLZAOWw4Xw==`, which pins
199
- `borgmcp-shared@0.7.1`, pass the complete local dogfood gate;
201
+ `sha512-0p/ZC03Y6G4N6bfBjMD5b+WcQXcLalhU/3QqGjSWYCFKa3q2/183O53w6Tu6y8h2DkhTw5prnamsOozmVpXZew==`;
202
+ - the coupled client/server release is published only after the server artifact
203
+ is rebuilt against `borgmcp-shared@0.8.0` and both pass the complete local
204
+ dogfood gate; the already-published server 0.8.0 remains immutable at its
205
+ older immutable shared dependency pin;
200
206
  - the selected stable client version is unused and the exact release commit is
201
207
  on protected `main`;
202
208
  - the repository and protected npm environment settings pass an operator audit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@modelcontextprotocol/sdk": "^1.0.4",
74
- "borgmcp-shared": "0.7.1",
74
+ "borgmcp-shared": "0.8.0",
75
75
  "chalk": "^5.3.0",
76
76
  "prompts": "^2.4.2",
77
77
  "which": "^4.0.0"
@@ -56,6 +56,17 @@ import {
56
56
  RepositoryAssociationOutcomeUnknownError,
57
57
  RepositoryAssociationResolutionError,
58
58
  } from './server-errors.js';
59
+ import {
60
+ decodeAndVerifyInvitationArtifact,
61
+ InvitationArtifactCompatibilityError,
62
+ InvitationArtifactEndpointMismatchError,
63
+ InvitationArtifactFormatError,
64
+ InvitationArtifactLegacyError,
65
+ InvitationArtifactRecoveryError,
66
+ InvitationArtifactStorageError,
67
+ InvitationArtifactTransportError,
68
+ InvitationArtifactTrustError,
69
+ } from './invitation-artifact.js';
59
70
  import type { SeatStatus } from './seat-probe.js';
60
71
  import type { ServerSessionOperation } from './config.js';
61
72
  import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
@@ -67,6 +78,7 @@ import type {
67
78
  AssociateRepositoryCubeResponse,
68
79
  CreateCubeRepository,
69
80
  CubeTemplate,
81
+ InvitationArtifact,
70
82
  } from 'borgmcp-shared/protocol';
71
83
  import {
72
84
  initializeRepositoryCube,
@@ -277,7 +289,11 @@ export interface AssimilateDeps {
277
289
  detectLocalServer: () => Promise<string | null>;
278
290
  connectServer: (
279
291
  apiUrl: string,
280
- enrollment?: { invitation: string; confirmReplacement?: () => Promise<boolean> },
292
+ enrollment?: {
293
+ invitation: string;
294
+ artifact?: import('borgmcp-shared/protocol').InvitationArtifact;
295
+ confirmReplacement?: () => Promise<boolean>;
296
+ },
281
297
  ) => Promise<{
282
298
  token: string;
283
299
  trustIdentity: string;
@@ -291,6 +307,17 @@ export interface AssimilateDeps {
291
307
  trustIdentity: string;
292
308
  serverCapabilities?: readonly string[];
293
309
  } | null>;
310
+ /** Read-only pending-artifact peek used to validate an explicit host before
311
+ * the normal resume path performs trust/credential work. */
312
+ peekPendingServerEnrollment?: () => Promise<{ origin: string; invitation: string } | null>;
313
+ resumePendingServerEnrollment?: (
314
+ onPending?: () => void,
315
+ ) => Promise<{
316
+ token: string;
317
+ apiUrl?: string;
318
+ trustIdentity: string;
319
+ serverCapabilities?: readonly string[];
320
+ } | null>;
294
321
 
295
322
  resolveRepositoryCube: (
296
323
  apiUrl: string,
@@ -382,6 +409,11 @@ async function selectAssimilationAuthority(
382
409
 
383
410
  // Only a local self-hosted server authority exists. Non-TTY and --yes must
384
411
  // NOT infer an authority — fail closed with actionable guidance.
412
+ if (flags.enroll && deps.isTTY()) {
413
+ // The v2 enrollment artifact is itself the authority selector. Use the
414
+ // local origin only as a placeholder until the hidden artifact is decoded.
415
+ return { kind: 'server', apiUrl: DEFAULT_LOCAL_SERVER_ORIGIN };
416
+ }
385
417
  if (!deps.isTTY() || flags.yes) {
386
418
  if (deps.defaultAuthority) return deps.defaultAuthority;
387
419
  const command = mode === 'cube-init'
@@ -521,6 +553,38 @@ function reportServerFailure(
521
553
  );
522
554
  return 1;
523
555
  }
556
+ if (error instanceof InvitationArtifactCompatibilityError) {
557
+ deps.stderr(`${error.message}\n`);
558
+ return 1;
559
+ }
560
+ if (error instanceof InvitationArtifactLegacyError) {
561
+ deps.stderr(`${error.message}\n`);
562
+ return 1;
563
+ }
564
+ if (error instanceof InvitationArtifactFormatError) {
565
+ deps.stderr(`${error.message}\n`);
566
+ return 1;
567
+ }
568
+ if (error instanceof InvitationArtifactEndpointMismatchError) {
569
+ deps.stderr(`${error.message}\n`);
570
+ return 1;
571
+ }
572
+ if (error instanceof InvitationArtifactTransportError) {
573
+ deps.stderr(`${error.message}\n`);
574
+ return 1;
575
+ }
576
+ if (error instanceof InvitationArtifactStorageError) {
577
+ deps.stderr(`${error.message}\n`);
578
+ return 1;
579
+ }
580
+ if (error instanceof InvitationArtifactRecoveryError) {
581
+ deps.stderr(`${error.message}\n`);
582
+ return 1;
583
+ }
584
+ if (error instanceof InvitationArtifactTrustError) {
585
+ deps.stderr(`${error.message}\n`);
586
+ return 1;
587
+ }
524
588
  if (/HTTP 40[13]|auth(?:entication|orization)|credential.*(?:invalid|rejected)/i.test(message)) {
525
589
  deps.stderr(
526
590
  `The saved enrollment for ${apiUrl} was rejected. Re-run ` +
@@ -662,7 +726,117 @@ export async function runAssimilate(
662
726
  return 1;
663
727
  }
664
728
 
665
- if (args.flags.server === undefined && deps.defaultAuthority === undefined) {
729
+ const artifactOnlyEnrollment = args.flags.enroll === true &&
730
+ args.flags.server === undefined && deps.defaultAuthority === undefined && deps.isTTY();
731
+ let preResumeAttempted = false;
732
+ let preResumedEnrollment: {
733
+ token: string;
734
+ trustIdentity: string;
735
+ serverCapabilities?: readonly string[];
736
+ } | null = null;
737
+
738
+ let prefetchedInvitation: string | undefined;
739
+ let prefetchedArtifact: InvitationArtifact | undefined;
740
+
741
+ // An explicit --host plus a new invitation must be rejected on the pure
742
+ // input path. Decode and compare the operator-presented artifact before any
743
+ // pending-enrollment lookup: the lookup enumerates the credential backend,
744
+ // so it must not precede this contradiction check. A matching artifact may
745
+ // then take the published pending-resume path until client#267 lands.
746
+ if (args.flags.enroll && args.flags.server !== undefined && deps.isTTY()) {
747
+ let preResumeOrigin: string;
748
+ try {
749
+ preResumeOrigin = normalizeServerEndpoint(args.flags.server);
750
+ } catch (error) {
751
+ deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
752
+ return 1;
753
+ }
754
+
755
+ prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
756
+ if (!prefetchedInvitation) {
757
+ deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
758
+ return 1;
759
+ }
760
+ try {
761
+ prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
762
+ if (prefetchedArtifact.endpoint !== preResumeOrigin) {
763
+ throw new InvitationArtifactEndpointMismatchError(
764
+ preResumeOrigin,
765
+ prefetchedArtifact.endpoint,
766
+ );
767
+ }
768
+ } catch (error) {
769
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
770
+ prefetchedInvitation = undefined;
771
+ prefetchedArtifact = undefined;
772
+ return 1;
773
+ }
774
+
775
+ let pendingForHost = false;
776
+ if (deps.peekPendingServerEnrollment !== undefined) {
777
+ let pending: { origin: string; invitation: string } | null = null;
778
+ try {
779
+ pending = await deps.peekPendingServerEnrollment();
780
+ } catch {
781
+ pending = null;
782
+ }
783
+ if (pending?.origin === preResumeOrigin) {
784
+ try {
785
+ const pendingArtifact = decodeAndVerifyInvitationArtifact(pending.invitation);
786
+ if (pendingArtifact.endpoint !== preResumeOrigin) {
787
+ throw new InvitationArtifactEndpointMismatchError(
788
+ preResumeOrigin,
789
+ pendingArtifact.endpoint,
790
+ );
791
+ }
792
+ pendingForHost = true;
793
+ } catch (error) {
794
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
795
+ return 1;
796
+ }
797
+ }
798
+ }
799
+
800
+ if (pendingForHost) {
801
+ try {
802
+ preResumeAttempted = true;
803
+ preResumedEnrollment = await deps.resumeServerEnrollment(preResumeOrigin, () => {
804
+ deps.stderr(
805
+ `Resuming the pending enrollment for \`${preResumeOrigin}\`; ` +
806
+ 'do not enter another invitation unless the server certificate was reissued; ' +
807
+ 'if it was, request a current invitation and rerun this command.\n',
808
+ );
809
+ });
810
+ } catch {
811
+ preResumeAttempted = false;
812
+ }
813
+ }
814
+ }
815
+ if (artifactOnlyEnrollment ||
816
+ preResumeAttempted && preResumedEnrollment === null) {
817
+ if (artifactOnlyEnrollment && deps.resumePendingServerEnrollment) {
818
+ preResumedEnrollment = await deps.resumePendingServerEnrollment(() => {
819
+ deps.stderr('Resuming the pending enrollment; no new invitation is required.\n');
820
+ });
821
+ }
822
+ if (artifactOnlyEnrollment && preResumedEnrollment) {
823
+ // The exact pending tuple was already redeemed or is being resumed.
824
+ } else {
825
+ prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
826
+ if (!prefetchedInvitation) {
827
+ deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
828
+ return 1;
829
+ }
830
+ try {
831
+ prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
832
+ } catch (error) {
833
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
834
+ prefetchedInvitation = undefined;
835
+ return 1;
836
+ }
837
+ }
838
+ }
839
+ if (!artifactOnlyEnrollment && args.flags.server === undefined && deps.defaultAuthority === undefined) {
666
840
  const connectCommand = mode === 'cube-init'
667
841
  ? 'borg server cube init --host <host>'
668
842
  : 'borg assimilate --host <host>';
@@ -699,8 +873,9 @@ export async function runAssimilate(
699
873
  }
700
874
 
701
875
  // ----- Step 1: Select and authenticate the local server -----
702
- const authority = await selectAssimilationAuthority(args.flags, deps, mode);
703
- if (!authority) return 1;
876
+ const selectedAuthority = await selectAssimilationAuthority(args.flags, deps, mode);
877
+ if (!selectedAuthority) return 1;
878
+ let authority: AssimilationAuthority = selectedAuthority;
704
879
  if (localSeatReadError !== undefined) {
705
880
  return reportServerFailure(deps, authority.apiUrl, localSeatReadError, false, mode);
706
881
  }
@@ -723,33 +898,48 @@ export async function runAssimilate(
723
898
  );
724
899
  return 1;
725
900
  }
726
- const resumed = await deps.resumeServerEnrollment(authority.apiUrl, () => {
727
- deps.stderr(
728
- `Resuming the pending enrollment for \`${authority.apiUrl}\`; ` +
729
- 'do not enter another invitation.\n',
730
- );
731
- });
901
+ let resumed = preResumedEnrollment;
902
+ if (!resumed && prefetchedArtifact === undefined && !preResumeAttempted && !artifactOnlyEnrollment) {
903
+ resumed = await deps.resumeServerEnrollment(authority.apiUrl, () => {
904
+ deps.stderr(
905
+ `Resuming the pending enrollment for \`${authority.apiUrl}\`; ` +
906
+ 'do not enter another invitation unless the server certificate was reissued; ' +
907
+ 'if it was, request a current invitation and rerun this command.\n',
908
+ );
909
+ });
910
+ }
732
911
  if (resumed) {
912
+ if ('apiUrl' in resumed && typeof resumed.apiUrl === 'string') {
913
+ authority = { kind: 'server', apiUrl: resumed.apiUrl };
914
+ }
733
915
  serverAuth = resumed;
734
916
  } else {
735
- let invitation = await deps.promptSecret(
736
- `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
917
+ let invitation = prefetchedInvitation ?? await deps.promptSecret(
918
+ artifactOnlyEnrollment
919
+ ? 'Enrollment invitation (single-use; hidden input):'
920
+ : `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
737
921
  );
738
922
  if (!invitation) {
739
- deps.stderr(
740
- `No enrollment invitation was entered for ${authority.apiUrl}. ` +
741
- `Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`,
742
- );
923
+ deps.stderr(artifactOnlyEnrollment
924
+ ? 'No enrollment invitation was entered. Ask the server operator for one, then rerun `borg assimilate --enroll`.\n'
925
+ : `No enrollment invitation was entered for ${authority.apiUrl}. ` +
926
+ `Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`);
743
927
  return 1;
744
928
  }
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
- });
929
+ try {
930
+ const artifact = prefetchedArtifact ?? decodeAndVerifyInvitationArtifact(invitation);
931
+ if (args.flags.server !== undefined && authority.apiUrl !== artifact.endpoint) {
932
+ throw new InvitationArtifactEndpointMismatchError(authority.apiUrl, artifact.endpoint);
933
+ }
934
+ authority = { kind: 'server', apiUrl: artifact.endpoint };
935
+ serverAuth = await deps.connectServer(authority.apiUrl, {
936
+ invitation,
937
+ artifact,
938
+ confirmReplacement: async () => strictAffirmative(await deps.prompt(
939
+ `A local enrollment for ${authority.apiUrl} already exists. Replacing it will orphan ` +
940
+ 'the first enrolled client. Replace it? [y/N]: ',
941
+ )),
942
+ });
753
943
  } finally {
754
944
  // Strings cannot be zeroized in JavaScript, but drop this command's
755
945
  // reference immediately after the exchange instead of retaining the
@@ -777,6 +967,11 @@ export async function runAssimilate(
777
967
  serverTrustIdentity: serverAuth.trustIdentity,
778
968
  serverCapabilities: serverAuth.serverCapabilities ?? [],
779
969
  };
970
+ if (args.flags.enroll) {
971
+ deps.stderr(
972
+ `This machine (${deps.getHostname()}) is enrolled with Borg server \`${authority.apiUrl}\`.\n`,
973
+ );
974
+ }
780
975
  } catch (error) {
781
976
  return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true, mode);
782
977
  }
@@ -34,6 +34,7 @@ import {
34
34
  connectLocalBorgServer,
35
35
  createLocalBorgServerCube,
36
36
  enrollLocalBorgServer,
37
+ enrollLocalBorgServerArtifact,
37
38
  probeLocalBorgServer,
38
39
  resolveLocalBorgServerRepositoryCube,
39
40
  resumeLocalBorgServerEnrollment,
@@ -62,6 +63,7 @@ import {
62
63
  setCodexWakeTarget,
63
64
  } from './cubes.js';
64
65
  import { addProjectSessionStartHook } from './config-utils.js';
66
+ import { findPendingServerEnrollment } from './config.js';
65
67
  import { setTerminalTitle as setTitle } from './terminal-title.js';
66
68
  import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
67
69
  import { prepareCodexRemoteLaunch, defaultCodexRemoteDeps } from './codex-remote.js';
@@ -240,6 +242,15 @@ export function buildDefaultAssimilateDeps(
240
242
  : null,
241
243
  connectServer: async (apiUrl, enrollment) => {
242
244
  if (enrollment) {
245
+ if (enrollment.artifact) {
246
+ return enrollLocalBorgServerArtifact(enrollment.artifact, {
247
+ invitation: enrollment.invitation,
248
+ ...(enrollment.confirmReplacement === undefined
249
+ ? {}
250
+ : { confirmReplacement: enrollment.confirmReplacement }),
251
+ clientName: osHostname(),
252
+ });
253
+ }
243
254
  return enrollLocalBorgServer(apiUrl, enrollment.invitation, {
244
255
  ...(enrollment.confirmReplacement === undefined
245
256
  ? {}
@@ -253,6 +264,21 @@ export function buildDefaultAssimilateDeps(
253
264
  resumeLocalBorgServerEnrollment(apiUrl, {
254
265
  ...(onPending === undefined ? {} : { onPending }),
255
266
  }),
267
+ peekPendingServerEnrollment: async () => {
268
+ const pending = await findPendingServerEnrollment();
269
+ return pending === null
270
+ ? null
271
+ : { origin: pending.origin, invitation: pending.invitation };
272
+ },
273
+ resumePendingServerEnrollment: async (onPending) =>
274
+ (async () => {
275
+ const pending = await findPendingServerEnrollment();
276
+ if (!pending) return null;
277
+ const result = await resumeLocalBorgServerEnrollment(pending.origin, {
278
+ ...(onPending === undefined ? {} : { onPending }),
279
+ });
280
+ return result ? { ...result, apiUrl: pending.origin } : null;
281
+ })(),
256
282
 
257
283
  resolveRepositoryCube: async (apiUrl, token, input, serverTrustIdentity) => {
258
284
  if (serverTrustIdentity === undefined) {
@@ -33,6 +33,7 @@ export function renderAssimilationWelcome(
33
33
  ``,
34
34
  `In the launched agent, run \`borg_whoami\` and \`borg_roster\` to verify the seat`,
35
35
  `and begin coordinating.`,
36
+ `Launch future sessions in this repository with \`borg\`.`,
36
37
  ...teammateLines,
37
38
  ``,
38
39
  ].join('\n');
package/src/claude.ts CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  } from './reset-local-seat-cmd.js';
44
44
  import { parseLaunchAllArgs } from './parse-launch-all-args.js';
45
45
  import { unknownSubcommand } from './unknown-subcommand.js';
46
+ import { parseRecoverEnrollmentArgs, runRecoverEnrollment } from './recover-enrollment-cmd.js';
46
47
  import { runLaunchAll } from './launch-all-cmd.js';
47
48
  import { buildDefaultLaunchAllDeps } from './launch-all-deps.js';
48
49
  import { discoverDroneCandidates } from './launch-all-discovery.js';
@@ -173,6 +174,27 @@ async function main() {
173
174
  const code = await runResetLocalSeat(parsed.flags, buildDefaultResetLocalSeatDeps());
174
175
  process.exit(code);
175
176
  }
177
+ if (process.argv[2] === 'recover-enrollment') {
178
+ const parsed = parseRecoverEnrollmentArgs(process.argv.slice(3));
179
+ if (!parsed.ok) {
180
+ if (parsed.error === 'help') {
181
+ process.stdout.write(clientSubcommandHelpText('recover-enrollment', [], getPackageVersion()) ?? '');
182
+ process.exit(0);
183
+ }
184
+ process.stderr.write(chalk.red(`${consolePrefix()}◼ borg recover-enrollment: ${parsed.error}\n`));
185
+ process.exit(1);
186
+ }
187
+ const prompt = async (message: string): Promise<string> => {
188
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
189
+ try { return await rl.question(message); } finally { rl.close(); }
190
+ };
191
+ const code = await runRecoverEnrollment(parsed.flags, {
192
+ prompt,
193
+ stderr: (line) => process.stderr.write(line),
194
+ stdout: (line) => process.stdout.write(line),
195
+ });
196
+ process.exit(code);
197
+ }
176
198
  if (process.argv[2] === 'spawn') {
177
199
  // Deprecated; the stub prints a redirect message and exits 2.
178
200
  const code = await runSpawn();
package/src/cli-help.ts CHANGED
@@ -64,6 +64,7 @@ export function clientSubcommandHelpText(
64
64
  case 'setup': return setupHelpText(version);
65
65
  case 'assimilate': return assimilateHelpText(version);
66
66
  case 'reset-local-seat': return resetLocalSeatHelpText(version);
67
+ case 'recover-enrollment': return recoverEnrollmentHelpText(version);
67
68
  case 'sync': return syncHelpText(version);
68
69
  case 'cleanup': return cleanupHelpText(version);
69
70
  case 'launch-all': return launchAllHelpText(version);
@@ -101,6 +102,7 @@ export function topLevelHelpText(version: string): string {
101
102
  ` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
102
103
  ` borg server cube init Initialize this repository's cube without creating a drone\n` +
103
104
  ` borg reset-local-seat Clear ONLY this worktree's saved local seat (offline; after a rejection)\n` +
105
+ ` borg recover-enrollment Clear ONLY one failed server enrollment transaction\n` +
104
106
  ` borg sync [--prune] Sync this worktree's branch to origin/main\n` +
105
107
  ` borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones\n` +
106
108
  ` borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)\n` +
@@ -113,6 +115,17 @@ export function topLevelHelpText(version: string): string {
113
115
  );
114
116
  }
115
117
 
118
+ export function recoverEnrollmentHelpText(version: string): string {
119
+ return (
120
+ `borg recover-enrollment (borgmcp ${version}) — clear one failed server enrollment\n\n` +
121
+ `Usage:\n` +
122
+ ` borg recover-enrollment [--host <host>] [--yes]\n` +
123
+ ` borg recover-enrollment --help\n\n` +
124
+ `This operation clears only the failed enrollment transaction for one server.\n` +
125
+ `It does not touch other server enrollments or accounts. It never requires manual file editing.\n`
126
+ );
127
+ }
128
+
116
129
  /** Help for the whole-product, npm-owned update journey. */
117
130
  export function updateHelpText(version: string): string {
118
131
  return (
@@ -143,6 +156,7 @@ export function serverHelpText(): string {
143
156
  ` status Report verified runtime evidence.\n` +
144
157
  ` update Verify and activate a local server artifact.\n` +
145
158
  ` invite Create a single-use invitation in an interactive terminal.\n` +
159
+ ` cert-reissue Widen the server certificate to cover another address without replacing the CA.\n` +
146
160
  ` dashboard View the running local server dashboard.\n` +
147
161
  ` cube init Initialize this Git repository's cube; does not create a drone.\n\n` +
148
162
  `Run borg server <command> --help for server command options.\n`