borgmcp 2.0.1 → 2.0.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.
- package/README.md +2 -2
- package/dist/codex-app-wake.d.ts +9 -11
- package/dist/codex-app-wake.d.ts.map +1 -1
- package/dist/codex-app-wake.js +15 -11
- package/dist/codex-app-wake.js.map +1 -1
- package/dist/codex-wake-resolve.d.ts +4 -4
- package/dist/codex-wake-resolve.js +4 -4
- package/dist/log-stream.d.ts +4 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +58 -2
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts +25 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +59 -4
- package/dist/remote-client.js.map +1 -1
- package/dist/seats.d.ts +72 -0
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +119 -0
- package/dist/seats.js.map +1 -1
- package/dist/server-errors.d.ts +1 -1
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js.map +1 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +49 -21
- package/dist/server-handshake.js.map +1 -1
- package/dist/session-continuity.d.ts +33 -0
- package/dist/session-continuity.d.ts.map +1 -0
- package/dist/session-continuity.js +220 -0
- package/dist/session-continuity.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +9 -7
- package/docs/LOCAL_SERVER.md +1 -1
- package/docs/RELEASING.md +13 -4
- package/package.json +2 -2
- package/src/codex-app-wake.ts +23 -11
- package/src/codex-wake-resolve.ts +4 -4
- package/src/log-stream.ts +64 -2
- package/src/remote-client.ts +92 -14
- package/src/seats.ts +170 -0
- package/src/server-errors.ts +3 -1
- package/src/server-handshake.ts +54 -21
- package/src/session-continuity.ts +280 -0
package/src/server-handshake.ts
CHANGED
|
@@ -40,7 +40,12 @@ import {
|
|
|
40
40
|
type SeatBinding,
|
|
41
41
|
type SeatOperation as ServerSessionOperation,
|
|
42
42
|
} from './seats.js';
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
BorgServerError,
|
|
45
|
+
BorgServerTrustError,
|
|
46
|
+
BorgServerUnreachableError,
|
|
47
|
+
} from './server-errors.js';
|
|
48
|
+
import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
|
|
44
49
|
import { readBoundedResponseBody } from './server-response.js';
|
|
45
50
|
import {
|
|
46
51
|
loadBorgServerTrust,
|
|
@@ -255,30 +260,36 @@ export async function sendBorgServerAttach(
|
|
|
255
260
|
const controller = new AbortController();
|
|
256
261
|
const timeout = setTimeout(() => controller.abort(), HANDSHAKE_TIMEOUT_MS);
|
|
257
262
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
263
|
+
let response: Response;
|
|
264
|
+
try {
|
|
265
|
+
response = await (deps.fetchImpl ?? fetch)(handshakeUrl(origin, ATTACH_PATH), {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
redirect: 'error',
|
|
268
|
+
signal: controller.signal,
|
|
269
|
+
headers: {
|
|
270
|
+
Accept: 'application/json',
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
Authorization: `Bearer ${parentCredential}`,
|
|
273
|
+
},
|
|
274
|
+
body: JSON.stringify(createAttachRequestEnvelope(randomUUID(), {
|
|
275
|
+
cube_id: request.cubeId,
|
|
276
|
+
role_id: request.roleId,
|
|
277
|
+
session_credential: pending.credential,
|
|
278
|
+
...(request.priorDroneId === undefined
|
|
279
|
+
? {}
|
|
280
|
+
: { prior_drone_id: request.priorDroneId }),
|
|
281
|
+
})),
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error instanceof BorgServerTrustError) throw error;
|
|
285
|
+
throw new BorgServerUnreachableError('Borg server attach transport failed', { cause: error });
|
|
286
|
+
}
|
|
287
|
+
if (response.status === 401 || response.status === 403 || response.status === 410) {
|
|
277
288
|
// A typed SESSION_REJECTED body means the presented bearer targets a seat
|
|
278
289
|
// already bound to a different session (takeover), distinct from a rejected
|
|
279
290
|
// parent enrollment credential. Decode defensively; any anomaly falls back
|
|
280
291
|
// to the generic credential rejection below. Never echo the response body.
|
|
281
|
-
if (response.status === 401) {
|
|
292
|
+
if (response.status === 401 || response.status === 410) {
|
|
282
293
|
let rejectedCode: ErrorCode | undefined;
|
|
283
294
|
try {
|
|
284
295
|
rejectedCode = decodeProtocolErrorEnvelope(
|
|
@@ -293,7 +304,23 @@ export async function sendBorgServerAttach(
|
|
|
293
304
|
'Borg server rejected the session: the seat is already bound to another session',
|
|
294
305
|
);
|
|
295
306
|
}
|
|
307
|
+
if (rejectedCode === ErrorCode.AUTH_EXPIRED) {
|
|
308
|
+
throw new BorgServerError(
|
|
309
|
+
'AUTH_EXPIRED',
|
|
310
|
+
'Borg server session expired',
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (rejectedCode === ErrorCode.SESSION_REVOKED) {
|
|
314
|
+
throw new BorgServerError(
|
|
315
|
+
'SESSION_REVOKED',
|
|
316
|
+
'Borg server session was revoked',
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (response.status === 410 && rejectedCode === DRONE_EVICTED_CODE) {
|
|
320
|
+
throw new DroneEvictedError();
|
|
321
|
+
}
|
|
296
322
|
}
|
|
323
|
+
if (response.status === 410) throw new Error('Borg server attach failed (HTTP 410)');
|
|
297
324
|
throw new BorgServerError('CREDENTIAL_REJECTED', 'Borg server enrollment was rejected');
|
|
298
325
|
}
|
|
299
326
|
if (response.status === 409) {
|
|
@@ -381,6 +408,12 @@ export async function sendBorgServerAttach(
|
|
|
381
408
|
...(binding.isHumanSeat !== undefined ? { isHumanSeat: binding.isHumanSeat } : {}),
|
|
382
409
|
}),
|
|
383
410
|
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error instanceof BorgServerTrustError) throw error;
|
|
413
|
+
if (controller.signal.aborted && !(error instanceof BorgServerUnreachableError)) {
|
|
414
|
+
throw new BorgServerUnreachableError('Borg server attach transport timed out', { cause: error });
|
|
415
|
+
}
|
|
416
|
+
throw error;
|
|
384
417
|
} finally {
|
|
385
418
|
clearTimeout(timeout);
|
|
386
419
|
}
|
|
@@ -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
|
+
}
|