borgmcp 2.0.1 → 2.0.3

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 (80) hide show
  1. package/README.md +16 -10
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +5 -0
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli-help.d.ts +2 -0
  7. package/dist/cli-help.d.ts.map +1 -1
  8. package/dist/cli-help.js +12 -0
  9. package/dist/cli-help.js.map +1 -1
  10. package/dist/codex-app-wake.d.ts +9 -11
  11. package/dist/codex-app-wake.d.ts.map +1 -1
  12. package/dist/codex-app-wake.js +15 -11
  13. package/dist/codex-app-wake.js.map +1 -1
  14. package/dist/codex-wake-resolve.d.ts +4 -4
  15. package/dist/codex-wake-resolve.js +4 -4
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +41 -18
  18. package/dist/config.js.map +1 -1
  19. package/dist/credential-paths.d.ts +3 -0
  20. package/dist/credential-paths.d.ts.map +1 -0
  21. package/dist/credential-paths.js +7 -0
  22. package/dist/credential-paths.js.map +1 -0
  23. package/dist/log-stream.d.ts +4 -0
  24. package/dist/log-stream.d.ts.map +1 -1
  25. package/dist/log-stream.js +58 -2
  26. package/dist/log-stream.js.map +1 -1
  27. package/dist/remote-client.d.ts +25 -0
  28. package/dist/remote-client.d.ts.map +1 -1
  29. package/dist/remote-client.js +59 -4
  30. package/dist/remote-client.js.map +1 -1
  31. package/dist/seat-store.d.ts +11 -5
  32. package/dist/seat-store.d.ts.map +1 -1
  33. package/dist/seat-store.js +147 -14
  34. package/dist/seat-store.js.map +1 -1
  35. package/dist/seats.d.ts +72 -0
  36. package/dist/seats.d.ts.map +1 -1
  37. package/dist/seats.js +119 -0
  38. package/dist/seats.js.map +1 -1
  39. package/dist/server-errors.d.ts +1 -1
  40. package/dist/server-errors.d.ts.map +1 -1
  41. package/dist/server-errors.js.map +1 -1
  42. package/dist/server-facade.d.ts +50 -0
  43. package/dist/server-facade.d.ts.map +1 -0
  44. package/dist/server-facade.js +126 -0
  45. package/dist/server-facade.js.map +1 -0
  46. package/dist/server-handshake.d.ts.map +1 -1
  47. package/dist/server-handshake.js +49 -21
  48. package/dist/server-handshake.js.map +1 -1
  49. package/dist/session-continuity.d.ts +33 -0
  50. package/dist/session-continuity.d.ts.map +1 -0
  51. package/dist/session-continuity.js +220 -0
  52. package/dist/session-continuity.js.map +1 -0
  53. package/dist/token-store.d.ts +2 -1
  54. package/dist/token-store.d.ts.map +1 -1
  55. package/dist/token-store.js +5 -3
  56. package/dist/token-store.js.map +1 -1
  57. package/dist/unknown-subcommand.d.ts +1 -1
  58. package/dist/unknown-subcommand.d.ts.map +1 -1
  59. package/dist/unknown-subcommand.js +1 -0
  60. package/dist/unknown-subcommand.js.map +1 -1
  61. package/docs/EXTRACTION_PROVENANCE.md +10 -7
  62. package/docs/LOCAL_SERVER.md +82 -43
  63. package/docs/RELEASING.md +22 -4
  64. package/package.json +2 -2
  65. package/src/claude.ts +5 -0
  66. package/src/cli-help.ts +15 -0
  67. package/src/codex-app-wake.ts +23 -11
  68. package/src/codex-wake-resolve.ts +4 -4
  69. package/src/config.ts +40 -18
  70. package/src/credential-paths.ts +8 -0
  71. package/src/log-stream.ts +64 -2
  72. package/src/remote-client.ts +92 -14
  73. package/src/seat-store.ts +159 -16
  74. package/src/seats.ts +170 -0
  75. package/src/server-errors.ts +3 -1
  76. package/src/server-facade.ts +192 -0
  77. package/src/server-handshake.ts +54 -21
  78. package/src/session-continuity.ts +280 -0
  79. package/src/token-store.ts +9 -4
  80. package/src/unknown-subcommand.ts +1 -0
@@ -0,0 +1,280 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { getServerCredential } from './config.js';
3
+ import { getActiveCube, type ActiveCube } from './cubes.js';
4
+ import { DroneEvictedError } from './drone-lifecycle.js';
5
+ import { sendBorgServerAttach } from './server-handshake.js';
6
+ import { BorgServerError, BorgServerTrustError } from './server-errors.js';
7
+ import { loadBorgServerTrust } from './server-trust.js';
8
+ import {
9
+ getActiveSeat,
10
+ prepareSeatReplacement,
11
+ promoteSeatReplacement,
12
+ refreshActiveSeatSession,
13
+ scrubSeatReplacement,
14
+ seatRef,
15
+ type SeatRecord,
16
+ } from './seats.js';
17
+
18
+ export const SESSION_RENEWAL_LEAD_MS = 5 * 60_000;
19
+
20
+ interface AttachOutcome {
21
+ result: 'created' | 'reused';
22
+ droneId: string;
23
+ sessionId: string;
24
+ expiresAt: string;
25
+ }
26
+
27
+ interface SessionContinuityDeps {
28
+ now(): number;
29
+ randomCredential(): string;
30
+ getSeat(ref: string, binding: { origin: string; trustIdentity: string; cubeId: string }): Promise<SeatRecord | null>;
31
+ getActive(): Promise<ActiveCube | null>;
32
+ getParentCredential(origin: string, trustIdentity: string): Promise<string | null>;
33
+ sendAttach(record: SeatRecord, bearer: string, parentCredential: string): Promise<AttachOutcome>;
34
+ prepareReplacement: typeof prepareSeatReplacement;
35
+ refreshSession: typeof refreshActiveSeatSession;
36
+ promoteReplacement: typeof promoteSeatReplacement;
37
+ scrubReplacement: typeof scrubSeatReplacement;
38
+ }
39
+
40
+ const defaultDeps: SessionContinuityDeps = {
41
+ now: Date.now,
42
+ randomCredential: () => randomBytes(32).toString('base64url'),
43
+ getSeat: getActiveSeat,
44
+ getActive: getActiveCube,
45
+ getParentCredential: getServerCredential,
46
+ async sendAttach(record, bearer, parentCredential) {
47
+ const trust = await loadBorgServerTrust(record.origin);
48
+ if (trust.identity !== record.trustIdentity) {
49
+ throw new BorgServerTrustError('Borg server trust identity changed; refusing session renewal');
50
+ }
51
+ const attached = await sendBorgServerAttach(
52
+ record.origin,
53
+ record.trustIdentity,
54
+ parentCredential,
55
+ {
56
+ cubeId: record.cubeId,
57
+ roleId: record.roleId,
58
+ operation: record.operation,
59
+ priorDroneId: record.droneId,
60
+ },
61
+ bearer,
62
+ { fetchImpl: trust.fetchImpl },
63
+ );
64
+ return {
65
+ result: attached.result,
66
+ droneId: attached.drone.id,
67
+ sessionId: attached.session.sessionId,
68
+ expiresAt: attached.session.expiresAt,
69
+ };
70
+ },
71
+ prepareReplacement: prepareSeatReplacement,
72
+ refreshSession: refreshActiveSeatSession,
73
+ promoteReplacement: promoteSeatReplacement,
74
+ scrubReplacement: scrubSeatReplacement,
75
+ };
76
+
77
+ const inflight = new Map<string, Promise<ActiveCube>>();
78
+ const digestOf = (value: string) => createHash('sha256').update(value).digest('hex');
79
+
80
+ function sameSeat(active: ActiveCube, candidate: ActiveCube | null): candidate is ActiveCube {
81
+ return candidate !== null &&
82
+ candidate.apiUrl === active.apiUrl &&
83
+ candidate.serverTrustIdentity === active.serverTrustIdentity &&
84
+ candidate.cubeId === active.cubeId &&
85
+ candidate.droneId === active.droneId &&
86
+ candidate.localSessionCredentialRef === active.localSessionCredentialRef;
87
+ }
88
+
89
+ function seatBinding(record: SeatRecord) {
90
+ return {
91
+ origin: record.origin,
92
+ trustIdentity: record.trustIdentity,
93
+ cubeId: record.cubeId,
94
+ };
95
+ }
96
+
97
+ function validActiveRecord(active: ActiveCube, record: SeatRecord | null): record is SeatRecord {
98
+ return record !== null &&
99
+ record.state === 'active' &&
100
+ record.origin === active.apiUrl &&
101
+ record.trustIdentity === active.serverTrustIdentity &&
102
+ record.cubeId === active.cubeId &&
103
+ record.droneId === active.droneId &&
104
+ seatRef(record) === active.localSessionCredentialRef;
105
+ }
106
+
107
+ function deterministicAttachFailure(error: unknown): boolean {
108
+ return error instanceof BorgServerError ||
109
+ error instanceof BorgServerTrustError ||
110
+ error instanceof DroneEvictedError;
111
+ }
112
+
113
+ async function reloadSameSeat(
114
+ active: ActiveCube,
115
+ deps: SessionContinuityDeps,
116
+ requireFreshCredential = false,
117
+ ): Promise<ActiveCube> {
118
+ const current = await deps.getActive();
119
+ if (!sameSeat(active, current) || (requireFreshCredential && current.sessionToken === active.sessionToken)) {
120
+ throw new Error('Session renewal no longer targets the same saved seat');
121
+ }
122
+ return current;
123
+ }
124
+
125
+ async function performRenewal(
126
+ active: ActiveCube,
127
+ mode: 'proactive' | 'expired',
128
+ deps: SessionContinuityDeps,
129
+ ): Promise<ActiveCube> {
130
+ if (!active.serverTrustIdentity || !active.localSessionCredentialRef) {
131
+ throw new Error('Selected Borg server authority state is missing or unreadable');
132
+ }
133
+ const binding = {
134
+ origin: active.apiUrl,
135
+ trustIdentity: active.serverTrustIdentity,
136
+ cubeId: active.cubeId,
137
+ };
138
+ const record = await deps.getSeat(active.localSessionCredentialRef, binding);
139
+ if (!validActiveRecord(active, record)) {
140
+ throw new Error('Session renewal no longer targets the same saved seat');
141
+ }
142
+ const activeDigest = digestOf(active.sessionToken);
143
+ if (digestOf(record.credential) !== activeDigest) {
144
+ return reloadSameSeat(active, deps, true);
145
+ }
146
+ const expiresIn = Date.parse(record.expiresAt!) - deps.now();
147
+ if (mode === 'proactive' && expiresIn > SESSION_RENEWAL_LEAD_MS && record.replacement === undefined) {
148
+ return active;
149
+ }
150
+ const parentCredential = await deps.getParentCredential(record.origin, record.trustIdentity);
151
+ if (!parentCredential) throw new Error('Selected Borg server parent credential is missing or unreadable');
152
+ const recordBinding = seatBinding(record);
153
+
154
+ let restoreRequired = mode === 'expired' || expiresIn <= 0 || record.replacement !== undefined;
155
+ if (!restoreRequired) {
156
+ let attached: AttachOutcome | null = null;
157
+ try {
158
+ attached = await deps.sendAttach(record, record.credential, parentCredential);
159
+ } catch (error) {
160
+ if (error instanceof BorgServerError && error.code === 'AUTH_EXPIRED') {
161
+ restoreRequired = true;
162
+ } else {
163
+ throw error;
164
+ }
165
+ }
166
+ if (!restoreRequired) {
167
+ if (!attached) throw new Error('Session renewal returned no result');
168
+ const renewedExpiry = Date.parse(attached.expiresAt);
169
+ if (
170
+ attached.result !== 'reused' ||
171
+ attached.droneId !== record.droneId ||
172
+ attached.sessionId !== record.sessionId ||
173
+ !Number.isFinite(renewedExpiry) ||
174
+ renewedExpiry <= Date.parse(record.expiresAt!)
175
+ ) {
176
+ throw new Error('Session renewal did not preserve the same saved seat');
177
+ }
178
+ const refreshed = await deps.refreshSession({
179
+ ref: active.localSessionCredentialRef,
180
+ binding: recordBinding,
181
+ expectedDroneId: record.droneId!,
182
+ expectedActiveDigest: activeDigest,
183
+ expectedSessionId: record.sessionId!,
184
+ expiresAt: attached.expiresAt,
185
+ });
186
+ if (!refreshed) return reloadSameSeat(active, deps);
187
+ return reloadSameSeat(active, deps);
188
+ }
189
+ }
190
+
191
+ const replacement = await deps.prepareReplacement({
192
+ ref: active.localSessionCredentialRef,
193
+ binding: recordBinding,
194
+ expectedDroneId: record.droneId!,
195
+ expectedActiveDigest: activeDigest,
196
+ replacementCredential: deps.randomCredential(),
197
+ });
198
+ if (!replacement.ok) return reloadSameSeat(active, deps, true);
199
+
200
+ let attached: AttachOutcome;
201
+ try {
202
+ attached = await deps.sendAttach(record, replacement.credential, parentCredential);
203
+ } catch (error) {
204
+ if (deterministicAttachFailure(error)) {
205
+ await deps.scrubReplacement({
206
+ ref: active.localSessionCredentialRef,
207
+ binding: recordBinding,
208
+ expectedActiveDigest: activeDigest,
209
+ expectedReplacementDigest: replacement.digest,
210
+ });
211
+ }
212
+ throw error;
213
+ }
214
+ const restoredExpiry = Date.parse(attached.expiresAt);
215
+ if (
216
+ (attached.result !== 'created' && attached.result !== 'reused') ||
217
+ attached.droneId !== record.droneId ||
218
+ attached.sessionId === record.sessionId ||
219
+ !Number.isFinite(restoredExpiry) ||
220
+ restoredExpiry <= deps.now()
221
+ ) {
222
+ await deps.scrubReplacement({
223
+ ref: active.localSessionCredentialRef,
224
+ binding: recordBinding,
225
+ expectedActiveDigest: activeDigest,
226
+ expectedReplacementDigest: replacement.digest,
227
+ });
228
+ throw new Error('Session renewal did not preserve the same saved seat');
229
+ }
230
+ const promoted = await deps.promoteReplacement({
231
+ ref: active.localSessionCredentialRef,
232
+ binding: recordBinding,
233
+ expectedDroneId: record.droneId!,
234
+ expectedActiveDigest: activeDigest,
235
+ expectedReplacementDigest: replacement.digest,
236
+ sessionId: attached.sessionId,
237
+ expiresAt: attached.expiresAt,
238
+ });
239
+ if (promoted !== 'promoted') return reloadSameSeat(active, deps, true);
240
+ return reloadSameSeat(active, deps, true);
241
+ }
242
+
243
+ function renewLocalSession(
244
+ active: ActiveCube,
245
+ mode: 'proactive' | 'expired',
246
+ deps: SessionContinuityDeps,
247
+ ): Promise<ActiveCube> {
248
+ const key = active.localSessionCredentialRef;
249
+ if (!key) return Promise.reject(new Error('Selected Borg server authority state is missing or unreadable'));
250
+ const existing = inflight.get(key);
251
+ if (existing) return existing;
252
+ const renewal = performRenewal(active, mode, deps);
253
+ inflight.set(key, renewal);
254
+ void renewal.finally(() => {
255
+ if (inflight.get(key) === renewal) inflight.delete(key);
256
+ }).catch(() => {});
257
+ return renewal;
258
+ }
259
+
260
+ export function ensureLocalSessionFresh(active: ActiveCube): Promise<ActiveCube> {
261
+ return renewLocalSession(active, 'proactive', defaultDeps);
262
+ }
263
+
264
+ export function recoverExpiredLocalSession(active: ActiveCube): Promise<ActiveCube> {
265
+ return renewLocalSession(active, 'expired', defaultDeps);
266
+ }
267
+
268
+ /** @internal */
269
+ export function __renewLocalSessionForTest(
270
+ active: ActiveCube,
271
+ mode: 'proactive' | 'expired',
272
+ deps: SessionContinuityDeps,
273
+ ): Promise<ActiveCube> {
274
+ return renewLocalSession(active, mode, deps);
275
+ }
276
+
277
+ /** @internal */
278
+ export function __resetSessionContinuityForTest(): void {
279
+ inflight.clear();
280
+ }
@@ -7,7 +7,8 @@
7
7
  * private keys; there is no keychain and no obfuscation-grade fallback.
8
8
  */
9
9
 
10
- import { atomicWrite0600, readStoreFile } from './seat-store.js';
10
+ import { atomicWrite0600, readStoreFile, type SecureStoreOptions } from './seat-store.js';
11
+ import { dirname } from 'node:path';
11
12
 
12
13
  // The OS-keychain backend was retired with the Queen rescope; the only backend
13
14
  // name is the 0600 file store.
@@ -37,9 +38,13 @@ export interface TokenBackend {
37
38
  * lockfile. Pure reads (get) are safe lock-free because atomicWrite0600's rename
38
39
  * guarantees a reader only ever sees a complete file.
39
40
  */
40
- export function makeFileBackend(filePath: string): TokenBackend {
41
+ export function makeFileBackend(
42
+ filePath: string,
43
+ storeOptions: SecureStoreOptions = {},
44
+ ): TokenBackend {
45
+ const options = { secureRoot: dirname(filePath), ...storeOptions };
41
46
  const load = async (): Promise<Record<string, string>> => {
42
- const raw = await readStoreFile(filePath);
47
+ const raw = await readStoreFile(filePath, options);
43
48
  // CR4 fail-closed: ONLY a missing file initializes empty. A present-but-
44
49
  // malformed / wrong-version / schema-invalid credential store MUST NOT read as
45
50
  // empty — a subsequent set/delete would OVERWRITE it and erase every stored
@@ -79,7 +84,7 @@ export function makeFileBackend(filePath: string): TokenBackend {
79
84
  return { ...(accounts as Record<string, string>) };
80
85
  };
81
86
  const save = (accounts: Record<string, string>): Promise<void> =>
82
- atomicWrite0600(filePath, JSON.stringify({ version: 1, accounts }, null, 2) + '\n');
87
+ atomicWrite0600(filePath, JSON.stringify({ version: 1, accounts }, null, 2) + '\n', options);
83
88
  return {
84
89
  name: 'file',
85
90
  async get(account) {
@@ -20,6 +20,7 @@ export const KNOWN_SUBCOMMANDS = [
20
20
  'sync',
21
21
  'cleanup',
22
22
  'launch-all',
23
+ 'server',
23
24
  ] as const;
24
25
 
25
26
  /**