borgmcp 2.9.0 → 2.10.2

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 +177 -11
  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 +13 -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 +13 -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 +47 -7
  69. package/package.json +2 -2
  70. package/src/assimilate-cmd.ts +231 -26
  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 +16 -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 +13 -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, and `2.10.0` was subsequently published, and the immutable `2.10.1` release attempt failed before artifact creation or publication, so the next candidate identity is `2.10.2`. 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.10.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.2` 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,25 @@ 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 next candidate
182
- uses the unused `v2.9.0` identity from a fresh reviewed protected-main commit
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==`.
186
+ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.10.0` tag object
187
+ `83bd13fe9943c980760db6ecdbcef0d8c413cee0` peels to protected-main commit
188
+ `c5d3d2cbe85644d402dfd82ce108dceca3a913a1`. Workflow run `30764262636`, attempt 1,
189
+ successfully published that exact source as `borgmcp@2.10.0`; the same-run artifact report records integrity
190
+ `sha512-YUVxWevAToQYwu/MDV2r9TrKnaZxpYiEitmLtr7OekfyE0+XZnkyHmL1gSk04JAiYquzv6ZyTRDYMnjQzS5GxA==`.
191
+ Never move, replace, reuse, or rerun that tag or workflow. FAILED-SUPERSEDED release record: The annotated `v2.10.1` tag object
192
+ `afee30802629d0f838b2446efd2241ff01964af2` peels to protected-main commit
193
+ `fcecd75211107994ccd0df0bf4093240f83de9b2`. Workflow run `30766475027`, attempt 1, concluded `failure`
194
+ during verification before artifact creation or publication. Verify job `91545993819` records the release
195
+ artifact build, verification, exercise, and upload steps as skipped; publish job `91546125556` was skipped.
196
+ Registry verification found no published `borgmcp@2.10.1` package, so no published npm artifact or SRI
197
+ exists for that version as of this check.
198
+ Never delete, move, replace, reuse, or rerun that tag, version, or workflow. The next candidate
199
+ uses the unused `v2.10.2` identity from a fresh reviewed protected-main commit
183
200
  and requires the complete release gate again.
184
201
 
185
202
  ## Release Prerequisites
@@ -191,12 +208,13 @@ Before creating the release tag, independently verify all of these conditions:
191
208
  - the extraction review confirms no private backend secrets, deployment
192
209
  configuration, customer data, local state, or duplicated shared contracts
193
210
  entered the public package;
194
- - the exact audited registry dependency `borgmcp-shared@0.7.1` remains locked to
211
+ - the exact audited registry dependency `borgmcp-shared@0.8.0` remains locked to
195
212
  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;
213
+ `sha512-0p/ZC03Y6G4N6bfBjMD5b+WcQXcLalhU/3QqGjSWYCFKa3q2/183O53w6Tu6y8h2DkhTw5prnamsOozmVpXZew==`;
214
+ - the coupled client/server release is published only after the server artifact
215
+ is rebuilt against `borgmcp-shared@0.8.0` and both pass the complete local
216
+ dogfood gate; the already-published server 0.8.0 remains immutable at its
217
+ older immutable shared dependency pin;
200
218
  - the selected stable client version is unused and the exact release commit is
201
219
  on protected `main`;
202
220
  - the repository and protected npm environment settings pass an operator audit;
@@ -228,6 +246,28 @@ anything. It then deterministically updates only the manifest version, the two
228
246
  root lockfile version fields, the extraction ledger, this document's immutable
229
247
  release record, and the release-lane current-version/evidence assertions.
230
248
 
249
+ When the superseded tag's attempt failed before artifact creation or publication,
250
+ prepare the fresh recovery identity with `--workflow-conclusion failure` and omit
251
+ `--artifact-integrity` entirely. The preparer verifies the annotated tag and exact
252
+ failed workflow attempt, writes a canonical failed-superseded record stating that
253
+ the artifact build, verification, exercise, upload, and publish phases were all
254
+ skipped, and separately re-verifies the last published release as the artifact
255
+ provenance anchor. It also reads the authoritative registry version list before
256
+ writing the time-bounded fact that no published npm artifact or SRI exists for
257
+ the failed version. This recovery-time absence check is required before preparing
258
+ the replacement identity; it is not the prohibited post-publication readback of
259
+ the newly published release. A failed record carrying an SRI, a failed run whose
260
+ artifact or publish phase ran, a failed version present in the registry, or a
261
+ successful record without an SRI is invalid. Failed tags and workflow attempts
262
+ stay immutable and must never be moved, reused, or rerun.
263
+
264
+ ```sh
265
+ npm run release:prepare -- 2.10.2 \
266
+ --workflow-run-id 30766475027 \
267
+ --workflow-run-attempt 1 \
268
+ --workflow-conclusion failure
269
+ ```
270
+
231
271
  Release branches use the `release/` prefix and enter protected `main` through a
232
272
  pull request. A direct push only updates the staging branch; it does
233
273
  not produce a release-identity verdict and grants no review skip, merge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "2.9.0",
3
+ "version": "2.10.2",
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'
@@ -429,12 +461,13 @@ async function selectAssimilationAuthority(
429
461
  }
430
462
 
431
463
  function localAssimilateCommand(
432
- apiUrl: string,
464
+ apiUrl: string | undefined,
433
465
  enroll = false,
434
466
  mode: 'assimilate' | 'cube-init' = 'assimilate',
435
467
  ): string {
436
468
  const command = mode === 'cube-init' ? 'borg server cube init' : 'borg assimilate';
437
- return `\`${command} --host ${apiUrl}${enroll ? ' --enroll' : ''}\``;
469
+ const host = apiUrl === undefined ? '' : ` --host ${apiUrl}`;
470
+ return `\`${command}${host}${enroll ? ' --enroll' : ''}\``;
438
471
  }
439
472
 
440
473
  function localAssimilateRoleCommand(apiUrl: string): string {
@@ -521,6 +554,38 @@ function reportServerFailure(
521
554
  );
522
555
  return 1;
523
556
  }
557
+ if (error instanceof InvitationArtifactCompatibilityError) {
558
+ deps.stderr(`${error.message}\n`);
559
+ return 1;
560
+ }
561
+ if (error instanceof InvitationArtifactLegacyError) {
562
+ deps.stderr(`${error.message}\n`);
563
+ return 1;
564
+ }
565
+ if (error instanceof InvitationArtifactFormatError) {
566
+ deps.stderr(`${error.message}\n`);
567
+ return 1;
568
+ }
569
+ if (error instanceof InvitationArtifactEndpointMismatchError) {
570
+ deps.stderr(`${error.message}\n`);
571
+ return 1;
572
+ }
573
+ if (error instanceof InvitationArtifactTransportError) {
574
+ deps.stderr(`${error.message}\n`);
575
+ return 1;
576
+ }
577
+ if (error instanceof InvitationArtifactStorageError) {
578
+ deps.stderr(`${error.message}\n`);
579
+ return 1;
580
+ }
581
+ if (error instanceof InvitationArtifactRecoveryError) {
582
+ deps.stderr(`${error.message}\n`);
583
+ return 1;
584
+ }
585
+ if (error instanceof InvitationArtifactTrustError) {
586
+ deps.stderr(`${error.message}\n`);
587
+ return 1;
588
+ }
524
589
  if (/HTTP 40[13]|auth(?:entication|orization)|credential.*(?:invalid|rejected)/i.test(message)) {
525
590
  deps.stderr(
526
591
  `The saved enrollment for ${apiUrl} was rejected. Re-run ` +
@@ -662,7 +727,126 @@ export async function runAssimilate(
662
727
  return 1;
663
728
  }
664
729
 
665
- if (args.flags.server === undefined && deps.defaultAuthority === undefined) {
730
+ const hostlessEnrollment = args.flags.enroll === true &&
731
+ args.flags.server === undefined && deps.defaultAuthority === undefined;
732
+ const artifactOnlyEnrollment = hostlessEnrollment && deps.isTTY();
733
+ let preResumeAttempted = false;
734
+ let preResumedEnrollment: {
735
+ token: string;
736
+ trustIdentity: string;
737
+ serverCapabilities?: readonly string[];
738
+ } | null = null;
739
+
740
+ let prefetchedInvitation: string | undefined;
741
+ let prefetchedArtifact: InvitationArtifact | undefined;
742
+
743
+ if (hostlessEnrollment && !deps.isTTY()) {
744
+ deps.stderr(
745
+ 'Local enrollment requires an interactive operator terminal. ' +
746
+ `Re-run ${localAssimilateCommand(undefined, true, mode)} from the operator’s terminal.\n`,
747
+ );
748
+ return 1;
749
+ }
750
+
751
+ // An explicit --host plus a new invitation must be rejected on the pure
752
+ // input path. Decode and compare the operator-presented artifact before any
753
+ // pending-enrollment lookup: the lookup enumerates the credential backend,
754
+ // so it must not precede this contradiction check. A matching artifact may
755
+ // then take the published pending-resume path until client#267 lands.
756
+ if (args.flags.enroll && args.flags.server !== undefined && deps.isTTY()) {
757
+ let preResumeOrigin: string;
758
+ try {
759
+ preResumeOrigin = normalizeServerEndpoint(args.flags.server);
760
+ } catch (error) {
761
+ deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
762
+ return 1;
763
+ }
764
+
765
+ prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
766
+ if (!prefetchedInvitation) {
767
+ deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
768
+ return 1;
769
+ }
770
+ try {
771
+ prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
772
+ if (prefetchedArtifact.endpoint !== preResumeOrigin) {
773
+ throw new InvitationArtifactEndpointMismatchError(
774
+ preResumeOrigin,
775
+ prefetchedArtifact.endpoint,
776
+ );
777
+ }
778
+ } catch (error) {
779
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
780
+ prefetchedInvitation = undefined;
781
+ prefetchedArtifact = undefined;
782
+ return 1;
783
+ }
784
+
785
+ let pendingForHost = false;
786
+ if (deps.peekPendingServerEnrollment !== undefined) {
787
+ let pending: { origin: string; invitation: string } | null = null;
788
+ try {
789
+ pending = await deps.peekPendingServerEnrollment();
790
+ } catch {
791
+ pending = null;
792
+ }
793
+ if (pending?.origin === preResumeOrigin) {
794
+ try {
795
+ const pendingArtifact = decodeAndVerifyInvitationArtifact(pending.invitation);
796
+ if (pendingArtifact.endpoint !== preResumeOrigin) {
797
+ throw new InvitationArtifactEndpointMismatchError(
798
+ preResumeOrigin,
799
+ pendingArtifact.endpoint,
800
+ );
801
+ }
802
+ pendingForHost = true;
803
+ } catch (error) {
804
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
805
+ return 1;
806
+ }
807
+ }
808
+ }
809
+
810
+ if (pendingForHost) {
811
+ try {
812
+ preResumeAttempted = true;
813
+ preResumedEnrollment = await deps.resumeServerEnrollment(preResumeOrigin, () => {
814
+ deps.stderr(
815
+ `Resuming the pending enrollment for \`${preResumeOrigin}\`; ` +
816
+ 'do not enter another invitation unless the server certificate was reissued; ' +
817
+ 'if it was, request a current invitation and rerun this command.\n',
818
+ );
819
+ });
820
+ } catch {
821
+ preResumeAttempted = false;
822
+ }
823
+ }
824
+ }
825
+ if (artifactOnlyEnrollment ||
826
+ preResumeAttempted && preResumedEnrollment === null) {
827
+ if (artifactOnlyEnrollment && deps.resumePendingServerEnrollment) {
828
+ preResumedEnrollment = await deps.resumePendingServerEnrollment(() => {
829
+ deps.stderr('Resuming the pending enrollment; no new invitation is required.\n');
830
+ });
831
+ }
832
+ if (artifactOnlyEnrollment && preResumedEnrollment) {
833
+ // The exact pending tuple was already redeemed or is being resumed.
834
+ } else {
835
+ prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
836
+ if (!prefetchedInvitation) {
837
+ deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
838
+ return 1;
839
+ }
840
+ try {
841
+ prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
842
+ } catch (error) {
843
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
844
+ prefetchedInvitation = undefined;
845
+ return 1;
846
+ }
847
+ }
848
+ }
849
+ if (!artifactOnlyEnrollment && args.flags.server === undefined && deps.defaultAuthority === undefined) {
666
850
  const connectCommand = mode === 'cube-init'
667
851
  ? 'borg server cube init --host <host>'
668
852
  : 'borg assimilate --host <host>';
@@ -699,8 +883,9 @@ export async function runAssimilate(
699
883
  }
700
884
 
701
885
  // ----- Step 1: Select and authenticate the local server -----
702
- const authority = await selectAssimilationAuthority(args.flags, deps, mode);
703
- if (!authority) return 1;
886
+ const selectedAuthority = await selectAssimilationAuthority(args.flags, deps, mode);
887
+ if (!selectedAuthority) return 1;
888
+ let authority: AssimilationAuthority = selectedAuthority;
704
889
  if (localSeatReadError !== undefined) {
705
890
  return reportServerFailure(deps, authority.apiUrl, localSeatReadError, false, mode);
706
891
  }
@@ -723,33 +908,48 @@ export async function runAssimilate(
723
908
  );
724
909
  return 1;
725
910
  }
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
- });
911
+ let resumed = preResumedEnrollment;
912
+ if (!resumed && prefetchedArtifact === undefined && !preResumeAttempted && !artifactOnlyEnrollment) {
913
+ resumed = await deps.resumeServerEnrollment(authority.apiUrl, () => {
914
+ deps.stderr(
915
+ `Resuming the pending enrollment for \`${authority.apiUrl}\`; ` +
916
+ 'do not enter another invitation unless the server certificate was reissued; ' +
917
+ 'if it was, request a current invitation and rerun this command.\n',
918
+ );
919
+ });
920
+ }
732
921
  if (resumed) {
922
+ if ('apiUrl' in resumed && typeof resumed.apiUrl === 'string') {
923
+ authority = { kind: 'server', apiUrl: resumed.apiUrl };
924
+ }
733
925
  serverAuth = resumed;
734
926
  } else {
735
- let invitation = await deps.promptSecret(
736
- `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
927
+ let invitation = prefetchedInvitation ?? await deps.promptSecret(
928
+ artifactOnlyEnrollment
929
+ ? 'Enrollment invitation (single-use; hidden input):'
930
+ : `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
737
931
  );
738
932
  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
- );
933
+ deps.stderr(artifactOnlyEnrollment
934
+ ? 'No enrollment invitation was entered. Ask the server operator for one, then rerun `borg assimilate --enroll`.\n'
935
+ : `No enrollment invitation was entered for ${authority.apiUrl}. ` +
936
+ `Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`);
743
937
  return 1;
744
938
  }
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
- });
939
+ try {
940
+ const artifact = prefetchedArtifact ?? decodeAndVerifyInvitationArtifact(invitation);
941
+ if (args.flags.server !== undefined && authority.apiUrl !== artifact.endpoint) {
942
+ throw new InvitationArtifactEndpointMismatchError(authority.apiUrl, artifact.endpoint);
943
+ }
944
+ authority = { kind: 'server', apiUrl: artifact.endpoint };
945
+ serverAuth = await deps.connectServer(authority.apiUrl, {
946
+ invitation,
947
+ artifact,
948
+ confirmReplacement: async () => strictAffirmative(await deps.prompt(
949
+ `A local enrollment for ${authority.apiUrl} already exists. Replacing it will orphan ` +
950
+ 'the first enrolled client. Replace it? [y/N]: ',
951
+ )),
952
+ });
753
953
  } finally {
754
954
  // Strings cannot be zeroized in JavaScript, but drop this command's
755
955
  // reference immediately after the exchange instead of retaining the
@@ -777,6 +977,11 @@ export async function runAssimilate(
777
977
  serverTrustIdentity: serverAuth.trustIdentity,
778
978
  serverCapabilities: serverAuth.serverCapabilities ?? [],
779
979
  };
980
+ if (args.flags.enroll) {
981
+ deps.stderr(
982
+ `This machine (${deps.getHostname()}) is enrolled with Borg server \`${authority.apiUrl}\`.\n`,
983
+ );
984
+ }
780
985
  } catch (error) {
781
986
  return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true, mode);
782
987
  }
@@ -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();