borgmcp 2.0.3 → 2.0.4
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/assimilate-cmd.d.ts +0 -2
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +51 -43
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +0 -1
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/cleanup-cmd.d.ts +1 -1
- package/dist/cleanup-cmd.d.ts.map +1 -1
- package/dist/cleanup-cmd.js +3 -1
- package/dist/cleanup-cmd.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +2 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/cubes.d.ts +0 -2
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +0 -2
- package/dist/cubes.js.map +1 -1
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +13 -7
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/log-stream.d.ts +0 -2
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +3 -30
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -23
- package/dist/remote-client.js.map +1 -1
- package/dist/seat-probe.d.ts +4 -4
- package/dist/seat-probe.d.ts.map +1 -1
- package/dist/seat-probe.js +2 -0
- package/dist/seat-probe.js.map +1 -1
- package/dist/seats.d.ts +0 -74
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +31 -131
- package/dist/seats.js.map +1 -1
- package/dist/server-errors.d.ts +6 -1
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +9 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +1 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +2 -2
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +0 -2
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +0 -5
- package/dist/server-handshake.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +5 -5
- package/docs/LOCAL_SERVER.md +7 -3
- package/docs/RELEASING.md +10 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +54 -51
- package/src/assimilate-deps.ts +0 -1
- package/src/cleanup-cmd.ts +5 -2
- package/src/cli-help.ts +2 -1
- package/src/cubes.ts +0 -4
- package/src/launch-all-cmd.ts +15 -7
- package/src/log-stream.ts +2 -30
- package/src/remote-client.ts +10 -22
- package/src/seat-probe.ts +5 -3
- package/src/seats.ts +35 -186
- package/src/server-errors.ts +9 -2
- package/src/server-facade.ts +2 -2
- package/src/server-handshake.ts +1 -10
- package/dist/session-continuity.d.ts +0 -33
- package/dist/session-continuity.d.ts.map +0 -1
- package/dist/session-continuity.js +0 -220
- package/dist/session-continuity.js.map +0 -1
- package/src/session-continuity.ts +0 -280
|
@@ -19,9 +19,9 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
|
|
|
19
19
|
repository-idempotent cube creation required for local dogfood.
|
|
20
20
|
- Initially set the standalone package identity to `2.0.0`. Its immutable
|
|
21
21
|
lightweight release tag failed before packaging. The reviewed `2.0.1`
|
|
22
|
-
recovery and `2.0.
|
|
23
|
-
candidate identity is `2.0.
|
|
24
|
-
publication.
|
|
22
|
+
recovery, `2.0.2`, and `2.0.3` successors were published and registry-verified;
|
|
23
|
+
the next candidate identity is `2.0.4`. Extraction and versioning do not
|
|
24
|
+
authorize publication.
|
|
25
25
|
|
|
26
26
|
## Review Holds
|
|
27
27
|
|
|
@@ -38,6 +38,6 @@ contract, with a pre-request `PENDING` record in the local 0600-permission seat
|
|
|
38
38
|
store, exact-tuple ambiguous retry, and verified activation. The contract now
|
|
39
39
|
resolves to the audited registry `borgmcp-shared@0.4.3`. The matching
|
|
40
40
|
`borgmcp-server@0.1.8` release is published and registry-verified. Client
|
|
41
|
-
`borgmcp@2.0.
|
|
42
|
-
candidate remains gated by reviewed `v2.0.
|
|
41
|
+
`borgmcp@2.0.3` is published and registry-verified. Publication of the next
|
|
42
|
+
candidate remains gated by reviewed `v2.0.4` source, a fresh annotated tag, and
|
|
43
43
|
exact registry integrity and signature verification.
|
package/docs/LOCAL_SERVER.md
CHANGED
|
@@ -28,6 +28,7 @@ Usage: borg server <command> [arguments]
|
|
|
28
28
|
Commands:
|
|
29
29
|
setup Prepare local server identity and data; does not start the server.
|
|
30
30
|
start Start the verified server in the foreground.
|
|
31
|
+
stop Stop the managed local server.
|
|
31
32
|
status Report verified runtime evidence.
|
|
32
33
|
update Verify and activate a local server artifact.
|
|
33
34
|
invite Create a single-use invitation in an interactive terminal.
|
|
@@ -119,9 +120,12 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
|
|
|
119
120
|
- Rejected or expired invitation: keep the server running and mint a replacement
|
|
120
121
|
invitation with `borg server invite`, then rerun `borg assimilate --host
|
|
121
122
|
<server> --enroll` with the replacement invitation.
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
--host <server
|
|
123
|
+
- Revoked or superseded local session: run `borg reset-local-seat`, ask the
|
|
124
|
+
operator for a new invitation from `borg server invite`, then run `borg
|
|
125
|
+
assimilate --host <server> --enroll` with the server still running.
|
|
126
|
+
- Unloadable local seat: run `borg reset-local-seat --host <server>` to clear
|
|
127
|
+
ONLY this worktree's saved local seat, then rerun `borg assimilate --host
|
|
128
|
+
<server>` with the server still running.
|
|
125
129
|
- Unreachable server: start or restart it with `borg server start`, then
|
|
126
130
|
rerun `borg assimilate --host <server>`.
|
|
127
131
|
- Trust mismatch after an intentional server re-initialization: verify the
|
package/docs/RELEASING.md
CHANGED
|
@@ -29,7 +29,16 @@ successfully published that exact source as `borgmcp@2.0.2`; the registry record
|
|
|
29
29
|
integrity
|
|
30
30
|
`sha512-w0O0t/2wzeJpXEqb4jQGuqZeavEkfFKdz6poRT8q1TxDKHTlOlwo3auctE+X1kWU2s5JkSDcUiUdQdyLwu84IA==`.
|
|
31
31
|
Never move, replace, reuse, or rerun that tag or workflow. The next candidate
|
|
32
|
-
|
|
32
|
+
must use a fresh unused identity and requires the complete release gate again.
|
|
33
|
+
|
|
34
|
+
The annotated `v2.0.3` tag object
|
|
35
|
+
`47701127cbdba6499595ef0529de264b00a61840` peels to protected-main commit
|
|
36
|
+
`3e44e9c439c036850c6a9226d02876f4883501d9`. Workflow run `29851751102`
|
|
37
|
+
successfully published that exact source as `borgmcp@2.0.3`; the registry records
|
|
38
|
+
integrity
|
|
39
|
+
`sha512-3Iy4BSB+yq8F75w/gWiEU7D+mKCYYa5mIsn/iC/byy5CK/QsAWg10KgsBI3GlDBZul1CD3Qq+zpmVeK2qpIE1A==`.
|
|
40
|
+
Never move, replace, reuse, or rerun that tag or workflow. The next candidate
|
|
41
|
+
uses the unused `v2.0.4` identity from a fresh reviewed protected-main commit
|
|
33
42
|
and requires the complete release gate again.
|
|
34
43
|
|
|
35
44
|
## Release Prerequisites
|
package/package.json
CHANGED
package/src/assimilate-cmd.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { installBorgPlugin } from './opencode-plugin.js';
|
|
|
42
42
|
import { computeOpenCodePort, connectOpenCodeDrone, createOpenCodeLaunchKickoff, injectInitialKickoff } from './opencode-drone.js';
|
|
43
43
|
import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
44
44
|
import { normalizeServerEndpoint } from './server-endpoint.js';
|
|
45
|
-
import { BorgServerError } from './server-errors.js';
|
|
45
|
+
import { BorgServerError, LegacySessionCredentialCollisionError } from './server-errors.js';
|
|
46
46
|
import type { SeatStatus } from './seat-probe.js';
|
|
47
47
|
import type { ServerSessionOperation } from './config.js';
|
|
48
48
|
import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
|
|
@@ -92,7 +92,6 @@ export interface AssimilateResult {
|
|
|
92
92
|
role_id: string;
|
|
93
93
|
local_session?: {
|
|
94
94
|
credential_ref: string;
|
|
95
|
-
expires_at: string | null;
|
|
96
95
|
};
|
|
97
96
|
// Idempotent-reattach discriminant: 'reused' when the server resolved the
|
|
98
97
|
// client bearer to an existing seat, 'created' on a first/fresh attach.
|
|
@@ -126,7 +125,6 @@ export interface ActiveCube {
|
|
|
126
125
|
/** Verified local-server CA identity; absent until a local server is selected. */
|
|
127
126
|
serverTrustIdentity?: string;
|
|
128
127
|
localSessionCredentialRef?: string;
|
|
129
|
-
localSessionExpiresAt?: string | null;
|
|
130
128
|
// gh#899: assimilated role, persisted for connect-time tool-surface scoping
|
|
131
129
|
// (mirrors cubes.ts ActiveCube; optional → backward-compatible).
|
|
132
130
|
roleName?: string;
|
|
@@ -388,25 +386,25 @@ function reportServerFailure(
|
|
|
388
386
|
);
|
|
389
387
|
return 1;
|
|
390
388
|
}
|
|
389
|
+
if (error instanceof LegacySessionCredentialCollisionError) {
|
|
390
|
+
deps.stderr(
|
|
391
|
+
`Local session credential collision detected.\n` +
|
|
392
|
+
`No local credentials were changed.\n` +
|
|
393
|
+
`Next: run borg assimilate --host ${error.origin} --enroll.\n`,
|
|
394
|
+
);
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
391
397
|
// A pin-matched typed 401: the server verified its own identity but rejected
|
|
392
|
-
// THIS worktree's session bearer
|
|
398
|
+
// THIS worktree's session bearer with an explicit terminal outcome.
|
|
393
399
|
// Distinct from a protocol/version mismatch and from a rejected enrollment:
|
|
394
400
|
// only this worktree's saved local seat is affected, and recovery is scoped to
|
|
395
401
|
// this worktree — no server/trust-anchor/cube/other-worktree reset, no restart
|
|
396
402
|
// or version-alignment advice (#1082).
|
|
403
|
+
if (error instanceof BorgServerError && error.code === 'SESSION_REVOKED') {
|
|
404
|
+
return diagnoseSessionTermination(deps, apiUrl, 'revoked');
|
|
405
|
+
}
|
|
397
406
|
if (error instanceof BorgServerError && error.code === 'SESSION_REJECTED') {
|
|
398
|
-
|
|
399
|
-
// claims NO mutation of its own; it points at the dedicated OFFLINE
|
|
400
|
-
// `borg reset-local-seat` command (the only writer that clears a seat).
|
|
401
|
-
deps.stderr(
|
|
402
|
-
`This worktree's saved local seat on ${apiUrl} is no longer accepted — it was ` +
|
|
403
|
-
'revoked or taken over by another session. No Borg state changed. Run ' +
|
|
404
|
-
`${resetLocalSeatCommand(apiUrl)} to clear ONLY this worktree's saved seat ` +
|
|
405
|
-
'(offline; add `--yes` when non-interactive), then ask the server operator for a new ' +
|
|
406
|
-
'invitation — the server can stay running — and re-enroll with ' +
|
|
407
|
-
`${localAssimilateCommand(apiUrl, true)}.\n`,
|
|
408
|
-
);
|
|
409
|
-
return 1;
|
|
407
|
+
return diagnoseSessionTermination(deps, apiUrl, 'superseded');
|
|
410
408
|
}
|
|
411
409
|
if (error instanceof BorgServerError && error.code === 'INVITATION_REJECTED') {
|
|
412
410
|
deps.stderr(
|
|
@@ -482,34 +480,19 @@ function resetLocalSeatCommand(apiUrl: string): string {
|
|
|
482
480
|
return `\`borg reset-local-seat --host ${apiUrl}\``;
|
|
483
481
|
}
|
|
484
482
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
* clause 1). The local server verified its OWN pinned identity (this is reached
|
|
489
|
-
* only after a successful pinned-TLS attach — a pin MISMATCH throws a distinct
|
|
490
|
-
* trust error and never lands here), then rejected THIS worktree's saved session
|
|
491
|
-
* bearer (revoked, or taken over by another session). We stop and recommend the
|
|
492
|
-
* dedicated OFFLINE `borg reset-local-seat` command, which is the ONLY writer
|
|
493
|
-
* that clears a worktree's saved seat. Nothing here contacts the server further,
|
|
494
|
-
* emits unselected egress, or touches local state. The recovery is
|
|
495
|
-
* live-safe: the operator is never told to stop/restart the server.
|
|
496
|
-
*/
|
|
497
|
-
function diagnoseSessionRejected(
|
|
483
|
+
// Pin-matched terminal session diagnosis. This is intentionally output-only:
|
|
484
|
+
// only the explicit offline reset command may clear the saved local seat.
|
|
485
|
+
function diagnoseSessionTermination(
|
|
498
486
|
deps: AssimilateDeps,
|
|
499
487
|
apiUrl: string,
|
|
488
|
+
outcome: 'revoked' | 'superseded',
|
|
500
489
|
): number {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
'revoked or taken over by another session. No Borg state was changed: your pinned ' +
|
|
505
|
-
'server trust, this and every other worktree, and their cubes are all untouched.\n',
|
|
506
|
-
);
|
|
490
|
+
const message = outcome === 'revoked'
|
|
491
|
+
? 'Local session was revoked.'
|
|
492
|
+
: 'Local session was superseded by a newer enrollment.';
|
|
507
493
|
deps.stderr(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
'offline — it revokes nothing server-side. Then ask the server operator for a new ' +
|
|
511
|
-
'invitation (the server can stay running) and re-enroll with ' +
|
|
512
|
-
`${localAssimilateCommand(apiUrl, true)}.\n`,
|
|
494
|
+
`${message}\n` +
|
|
495
|
+
`Next: run borg reset-local-seat, then borg assimilate --host ${apiUrl} --enroll.\n`,
|
|
513
496
|
);
|
|
514
497
|
return 1;
|
|
515
498
|
}
|
|
@@ -534,9 +517,28 @@ export async function runAssimilate(
|
|
|
534
517
|
}
|
|
535
518
|
}
|
|
536
519
|
|
|
520
|
+
// Read local seat state before authority discovery, which may probe the local
|
|
521
|
+
// server. A retired replacement collision must not send either saved bearer or
|
|
522
|
+
// perform any other network request.
|
|
523
|
+
let existing: ActiveCube | null = null;
|
|
524
|
+
let hasPersistedIdentity = false;
|
|
525
|
+
let localSeatReadError: unknown;
|
|
526
|
+
try {
|
|
527
|
+
existing = await deps.getActiveCube();
|
|
528
|
+
hasPersistedIdentity = existing !== null || await deps.hasPersistedActiveCube();
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (error instanceof LegacySessionCredentialCollisionError) {
|
|
531
|
+
return reportServerFailure(deps, error.origin, error);
|
|
532
|
+
}
|
|
533
|
+
localSeatReadError = error;
|
|
534
|
+
}
|
|
535
|
+
|
|
537
536
|
// ----- Step 1: Select and authenticate the local server -----
|
|
538
537
|
const authority = await selectAssimilationAuthority(args.flags, deps);
|
|
539
538
|
if (!authority) return 1;
|
|
539
|
+
if (localSeatReadError !== undefined) {
|
|
540
|
+
return reportServerFailure(deps, authority.apiUrl, localSeatReadError);
|
|
541
|
+
}
|
|
540
542
|
|
|
541
543
|
// ----- Repository + cube-name preflight -----
|
|
542
544
|
// Resolve and, where necessary, confirm local presentation data before an
|
|
@@ -766,8 +768,6 @@ export async function runAssimilate(
|
|
|
766
768
|
// retain its original role so the attach request reuses the exact durable
|
|
767
769
|
// retry binding instead of selecting another unoccupied role and minting a
|
|
768
770
|
// duplicate seat.
|
|
769
|
-
const existing = await deps.getActiveCube();
|
|
770
|
-
const hasPersistedIdentity = existing !== null || await deps.hasPersistedActiveCube();
|
|
771
771
|
const wantSibling =
|
|
772
772
|
args.flags.worktree !== undefined || (existing !== null && !args.flags.here);
|
|
773
773
|
// `let`: the bound-pending resume path (CR#2) OVERRIDES this from the stored
|
|
@@ -943,8 +943,11 @@ export async function runAssimilate(
|
|
|
943
943
|
// rejection; it points at the offline `borg reset-local-seat` command.
|
|
944
944
|
// Distinct from unreachable/404/5xx/trust-mismatch, which stay
|
|
945
945
|
// indeterminate below.
|
|
946
|
+
if (status === 'revoked') {
|
|
947
|
+
return diagnoseSessionTermination(deps, auth.apiUrl, 'revoked');
|
|
948
|
+
}
|
|
946
949
|
if (status === 'rejected') {
|
|
947
|
-
return
|
|
950
|
+
return diagnoseSessionTermination(deps, auth.apiUrl, 'superseded');
|
|
948
951
|
}
|
|
949
952
|
// CR #6: distinct causes get cause-accurate, non-destructive recovery —
|
|
950
953
|
// never the generic "restart the server" advice.
|
|
@@ -1192,17 +1195,18 @@ export async function runAssimilate(
|
|
|
1192
1195
|
);
|
|
1193
1196
|
return 1;
|
|
1194
1197
|
}
|
|
1195
|
-
// Pin-matched
|
|
1198
|
+
// Pin-matched terminal session outcomes are pure diagnosis.
|
|
1196
1199
|
// Reached only after a successful pinned-TLS attach, so it is pin-matched by
|
|
1197
1200
|
// construction — a pin mismatch throws a distinct trust error and never
|
|
1198
1201
|
// enters this branch. Attach mutates NOTHING; it recommends the offline
|
|
1199
1202
|
// `borg reset-local-seat` command.
|
|
1200
|
-
if (
|
|
1201
|
-
err
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1203
|
+
if (err instanceof BorgServerError && reattachPriorId != null) {
|
|
1204
|
+
if (err.code === 'SESSION_REVOKED') {
|
|
1205
|
+
return diagnoseSessionTermination(deps, authority.apiUrl, 'revoked');
|
|
1206
|
+
}
|
|
1207
|
+
if (err.code === 'SESSION_REJECTED') {
|
|
1208
|
+
return diagnoseSessionTermination(deps, authority.apiUrl, 'superseded');
|
|
1209
|
+
}
|
|
1206
1210
|
}
|
|
1207
1211
|
if (authority.kind === 'server') {
|
|
1208
1212
|
return reportServerFailure(deps, authority.apiUrl, err);
|
|
@@ -1389,7 +1393,6 @@ export async function runAssimilate(
|
|
|
1389
1393
|
apiUrl: auth.apiUrl,
|
|
1390
1394
|
serverTrustIdentity: auth.serverTrustIdentity,
|
|
1391
1395
|
localSessionCredentialRef: result.local_session!.credential_ref,
|
|
1392
|
-
localSessionExpiresAt: result.local_session!.expires_at,
|
|
1393
1396
|
// gh#899: persist the assimilated role so the connect-time ListTools
|
|
1394
1397
|
// handler can role-scope the native tool surface.
|
|
1395
1398
|
roleName: assignedRole.name,
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -352,7 +352,6 @@ export function buildDefaultAssimilateDeps(): AssimilateDeps {
|
|
|
352
352
|
result: prepared.result,
|
|
353
353
|
local_session: {
|
|
354
354
|
credential_ref: prepared.credentialRef,
|
|
355
|
-
expires_at: prepared.session.expiresAt,
|
|
356
355
|
},
|
|
357
356
|
// Handles for the cube-lock-held FINALIZE (assimilate-cmd Step 8).
|
|
358
357
|
finalize: {
|
package/src/cleanup-cmd.ts
CHANGED
|
@@ -69,7 +69,8 @@ export type CleanupReason =
|
|
|
69
69
|
| 'SURVIVES-clobber'
|
|
70
70
|
| 'SURVIVES-unmerged'
|
|
71
71
|
| 'SURVIVES-detached'
|
|
72
|
-
| 'SURVIVES-
|
|
72
|
+
| 'SURVIVES-revoked'
|
|
73
|
+
| 'SURVIVES-superseded'
|
|
73
74
|
| 'SURVIVES-live'
|
|
74
75
|
| 'SURVIVES-self'
|
|
75
76
|
| 'UNKNOWN-indeterminate'
|
|
@@ -345,7 +346,9 @@ async function classifyWorktree(
|
|
|
345
346
|
case 'evicted':
|
|
346
347
|
return { reason: 'PRUNABLE', detail: '410 DRONE_EVICTED (clean + merged)' };
|
|
347
348
|
case 'rejected':
|
|
348
|
-
return { reason: 'SURVIVES-
|
|
349
|
+
return { reason: 'SURVIVES-superseded', detail: 'session superseded by a newer enrollment — recoverable via re-enroll, never delete' };
|
|
350
|
+
case 'revoked':
|
|
351
|
+
return { reason: 'SURVIVES-revoked', detail: 'session revoked — recoverable via re-enroll, never delete' };
|
|
349
352
|
case 'live':
|
|
350
353
|
return { reason: 'SURVIVES-live', detail: 'seat resolves (drone alive)' };
|
|
351
354
|
case 'indeterminate':
|
package/src/cli-help.ts
CHANGED
|
@@ -50,6 +50,7 @@ export function serverHelpText(): string {
|
|
|
50
50
|
`Commands:\n` +
|
|
51
51
|
` setup Prepare local server identity and data; does not start the server.\n` +
|
|
52
52
|
` start Start the verified server in the foreground.\n` +
|
|
53
|
+
` stop Stop the managed local server.\n` +
|
|
53
54
|
` status Report verified runtime evidence.\n` +
|
|
54
55
|
` update Verify and activate a local server artifact.\n` +
|
|
55
56
|
` invite Create a single-use invitation in an interactive terminal.\n\n` +
|
|
@@ -102,7 +103,7 @@ export function resetLocalSeatHelpText(version: string): string {
|
|
|
102
103
|
`just this worktree's saved local seat — its credential and cube binding together — from the\n` +
|
|
103
104
|
`local seat store on this machine. Server, trust anchor, cube, and every sibling worktree are\n` +
|
|
104
105
|
`left untouched.\n\n` +
|
|
105
|
-
`Use it after \`borg assimilate\` reports this worktree's
|
|
106
|
+
`Use it after \`borg assimilate\` reports this worktree's session was revoked or superseded\n` +
|
|
106
107
|
`(a pin-matched rejection), then ask the operator for a new invitation and re-enroll.\n\n` +
|
|
107
108
|
`Usage:\n` +
|
|
108
109
|
` borg reset-local-seat Reset this worktree's saved seat (TTY confirms [y/N])\n` +
|
package/src/cubes.ts
CHANGED
|
@@ -62,7 +62,6 @@ export interface ActiveCube {
|
|
|
62
62
|
serverTrustIdentity?: string;
|
|
63
63
|
/** Opaque local-session keychain reference; never a bearer. */
|
|
64
64
|
localSessionCredentialRef?: string;
|
|
65
|
-
localSessionExpiresAt?: string | null;
|
|
66
65
|
// gh#899: the assimilated role, persisted so the connect-time ListTools
|
|
67
66
|
// handler can role-scope the NATIVE tool surface (UX/context only — never an
|
|
68
67
|
// auth boundary). Absent on pre-gh#899 cubes.json entries → the filter
|
|
@@ -280,7 +279,6 @@ async function hydrateActiveCube(record: SeatRecord): Promise<ActiveCube | null>
|
|
|
280
279
|
apiUrl: record.origin,
|
|
281
280
|
serverTrustIdentity: record.trustIdentity,
|
|
282
281
|
localSessionCredentialRef: ref,
|
|
283
|
-
...(record.expiresAt !== undefined ? { localSessionExpiresAt: record.expiresAt } : {}),
|
|
284
282
|
...(record.roleName !== undefined ? { roleName: record.roleName } : {}),
|
|
285
283
|
...(record.roleClass !== undefined ? { roleClass: record.roleClass } : {}),
|
|
286
284
|
...(record.isHumanSeat !== undefined ? { isHumanSeat: record.isHumanSeat } : {}),
|
|
@@ -392,7 +390,6 @@ export interface PersistedLocalSeat {
|
|
|
392
390
|
apiUrl: string;
|
|
393
391
|
serverTrustIdentity: string;
|
|
394
392
|
localSessionCredentialRef: string;
|
|
395
|
-
localSessionExpiresAt?: string | null;
|
|
396
393
|
roleName?: string;
|
|
397
394
|
roleClass?: 'queen' | 'worker';
|
|
398
395
|
isHumanSeat?: boolean;
|
|
@@ -432,7 +429,6 @@ export async function readPersistedLocalSeat(): Promise<PersistedLocalSeat | nul
|
|
|
432
429
|
localSessionCredentialRef: seatRef(record),
|
|
433
430
|
operation: record.operation,
|
|
434
431
|
state: record.state,
|
|
435
|
-
...(record.expiresAt !== undefined ? { localSessionExpiresAt: record.expiresAt } : {}),
|
|
436
432
|
...(record.roleName !== undefined ? { roleName: record.roleName } : {}),
|
|
437
433
|
...(record.roleClass !== undefined ? { roleClass: record.roleClass } : {}),
|
|
438
434
|
...(record.isHumanSeat !== undefined ? { isHumanSeat: record.isHumanSeat } : {}),
|
package/src/launch-all-cmd.ts
CHANGED
|
@@ -276,6 +276,7 @@ export async function runLaunchAll(
|
|
|
276
276
|
// LOCK-live (seemingly-running) session.
|
|
277
277
|
const launchable: DroneCandidate[] = [];
|
|
278
278
|
let evictedCount = 0;
|
|
279
|
+
let revokedCount = 0;
|
|
279
280
|
let rejectedCount = 0;
|
|
280
281
|
let trustMismatchCount = 0;
|
|
281
282
|
let credentialRejectedCount = 0;
|
|
@@ -294,15 +295,19 @@ export async function runLaunchAll(
|
|
|
294
295
|
);
|
|
295
296
|
continue;
|
|
296
297
|
}
|
|
298
|
+
if (status === 'revoked') {
|
|
299
|
+
revokedCount += 1;
|
|
300
|
+
deps.stderr(
|
|
301
|
+
`Local session was revoked.\n` +
|
|
302
|
+
`Next: run borg reset-local-seat, then borg assimilate --host ${c.apiUrl} --enroll.\n`
|
|
303
|
+
);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
297
306
|
if (status === 'rejected') {
|
|
298
307
|
rejectedCount += 1;
|
|
299
308
|
deps.stderr(
|
|
300
|
-
`
|
|
301
|
-
`
|
|
302
|
-
`1) \`borg reset-local-seat --host ${c.apiUrl}\` (add --yes non-interactively) to clear ` +
|
|
303
|
-
`ONLY that worktree's saved seat; ` +
|
|
304
|
-
`2) ask the server operator to mint a fresh scoped invitation (the server stays running); ` +
|
|
305
|
-
`3) \`borg assimilate --host ${c.apiUrl} --enroll\`.\n`
|
|
309
|
+
`Local session was superseded by a newer enrollment.\n` +
|
|
310
|
+
`Next: run borg reset-local-seat, then borg assimilate --host ${c.apiUrl} --enroll.\n`
|
|
306
311
|
);
|
|
307
312
|
continue;
|
|
308
313
|
}
|
|
@@ -355,8 +360,11 @@ export async function runLaunchAll(
|
|
|
355
360
|
if (evictedCount > 0) {
|
|
356
361
|
causes.push(`${evictedCount} evicted`);
|
|
357
362
|
}
|
|
363
|
+
if (revokedCount > 0) {
|
|
364
|
+
causes.push(`${revokedCount} revoked`);
|
|
365
|
+
}
|
|
358
366
|
if (rejectedCount > 0) {
|
|
359
|
-
causes.push(`${rejectedCount}
|
|
367
|
+
causes.push(`${rejectedCount} superseded`);
|
|
360
368
|
}
|
|
361
369
|
if (trustMismatchCount > 0) {
|
|
362
370
|
causes.push(`${trustMismatchCount} with a changed (terminal) server identity`);
|
package/src/log-stream.ts
CHANGED
|
@@ -54,8 +54,7 @@ import {
|
|
|
54
54
|
wakeCodexViaAppServer,
|
|
55
55
|
} from './codex-app-wake.js';
|
|
56
56
|
import { readBoundedResponseBody } from './server-response.js';
|
|
57
|
-
import { BorgServerError
|
|
58
|
-
import { recoverExpiredLocalSession } from './session-continuity.js';
|
|
57
|
+
import { BorgServerError } from './server-errors.js';
|
|
59
58
|
import {
|
|
60
59
|
acquireStreamLease,
|
|
61
60
|
readOwnershipSnapshot,
|
|
@@ -428,7 +427,6 @@ export interface RunLoopTestDeps {
|
|
|
428
427
|
getActiveCube?: typeof getActiveCube;
|
|
429
428
|
acquireStreamLease?: typeof acquireStreamLease;
|
|
430
429
|
streamOnce?: typeof streamOnce;
|
|
431
|
-
recoverExpiredSession?: typeof recoverExpiredLocalSession;
|
|
432
430
|
sleep?: (ms: number) => Promise<void>;
|
|
433
431
|
maxIterations?: number;
|
|
434
432
|
}
|
|
@@ -437,7 +435,6 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
|
|
|
437
435
|
const _getActiveCube = testDeps.getActiveCube ?? getActiveCube;
|
|
438
436
|
const _acquireStreamLease = testDeps.acquireStreamLease ?? acquireStreamLease;
|
|
439
437
|
const _streamOnce = testDeps.streamOnce ?? streamOnce;
|
|
440
|
-
const _recoverExpiredSession = testDeps.recoverExpiredSession ?? recoverExpiredLocalSession;
|
|
441
438
|
const _sleep = testDeps.sleep ?? sleep;
|
|
442
439
|
const _maxIterations = testDeps.maxIterations ?? Infinity;
|
|
443
440
|
let _iterations = 0;
|
|
@@ -447,7 +444,6 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
|
|
|
447
444
|
let currentCubeId: string | null = null;
|
|
448
445
|
let lease: StreamLease | null = null;
|
|
449
446
|
let leaseKey: string | null = null;
|
|
450
|
-
let lastRecoveredToken: string | null = null;
|
|
451
447
|
|
|
452
448
|
try {
|
|
453
449
|
while (_iterations < _maxIterations) {
|
|
@@ -548,7 +544,6 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
|
|
|
548
544
|
}
|
|
549
545
|
// Clean disconnect (e.g. server-side rollout). Reset backoff.
|
|
550
546
|
attempt = 0;
|
|
551
|
-
lastRecoveredToken = null;
|
|
552
547
|
streamState.reconnectAttempts = 0;
|
|
553
548
|
} catch (err: any) {
|
|
554
549
|
if (ownerLost) {
|
|
@@ -576,27 +571,7 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
|
|
|
576
571
|
);
|
|
577
572
|
throw new TerminalStreamError();
|
|
578
573
|
}
|
|
579
|
-
if (err instanceof BorgServerError)
|
|
580
|
-
if (err.code !== 'AUTH_EXPIRED' || lastRecoveredToken === active.sessionToken) {
|
|
581
|
-
throw new TerminalStreamError();
|
|
582
|
-
}
|
|
583
|
-
try {
|
|
584
|
-
const renewed = await _recoverExpiredSession(active);
|
|
585
|
-
lastRecoveredToken = renewed.sessionToken;
|
|
586
|
-
attempt = 0;
|
|
587
|
-
streamState.reconnectAttempts = 0;
|
|
588
|
-
continue;
|
|
589
|
-
} catch (recoveryError) {
|
|
590
|
-
if (!(recoveryError instanceof BorgServerUnreachableError)) {
|
|
591
|
-
throw new TerminalStreamError();
|
|
592
|
-
}
|
|
593
|
-
// The replacement bearer is durable and intentionally survives an
|
|
594
|
-
// ambiguous transport failure. Fall through to the ordinary bounded
|
|
595
|
-
// reconnect backoff; the next AUTH_EXPIRED response resumes that exact
|
|
596
|
-
// replacement instead of permanently silencing the stream.
|
|
597
|
-
err = recoveryError;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
574
|
+
if (err instanceof BorgServerError) throw new TerminalStreamError();
|
|
600
575
|
streamState.connected = false;
|
|
601
576
|
const delay =
|
|
602
577
|
Math.min(RECONNECT_MIN_MS * 2 ** attempt, RECONNECT_MAX_MS) +
|
|
@@ -907,9 +882,6 @@ export async function streamOnce(
|
|
|
907
882
|
} catch {
|
|
908
883
|
code = undefined;
|
|
909
884
|
}
|
|
910
|
-
if (code === ErrorCode.AUTH_EXPIRED) {
|
|
911
|
-
throw new BorgServerError('AUTH_EXPIRED', 'Borg server session expired');
|
|
912
|
-
}
|
|
913
885
|
if (code === ErrorCode.SESSION_REJECTED) {
|
|
914
886
|
throw new BorgServerError('SESSION_REJECTED', 'Borg server session was rejected');
|
|
915
887
|
}
|
package/src/remote-client.ts
CHANGED
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
} from './local-server-cursor.js';
|
|
48
48
|
import { readBoundedResponseBody } from './server-response.js';
|
|
49
49
|
import { resolveLocalLogRecipients } from './local-log-routing.js';
|
|
50
|
-
import { ensureLocalSessionFresh, recoverExpiredLocalSession } from './session-continuity.js';
|
|
51
50
|
|
|
52
51
|
export interface RemoteConnection {
|
|
53
52
|
apiUrl: string;
|
|
@@ -229,15 +228,12 @@ async function localServerRequest<T>(
|
|
|
229
228
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH',
|
|
230
229
|
payload?: Record<string, unknown>,
|
|
231
230
|
): Promise<T | null> {
|
|
232
|
-
|
|
233
|
-
? await ensureLocalSessionFresh(active)
|
|
234
|
-
: active;
|
|
235
|
-
const request = (candidate: ActiveCube) => decodeLocalProtocolResponse<T>((signal) => authedFetch(path, {
|
|
231
|
+
return decodeLocalProtocolResponse<T>((signal) => authedFetch(path, {
|
|
236
232
|
method,
|
|
237
233
|
signal,
|
|
238
|
-
droneSession:
|
|
239
|
-
apiUrl:
|
|
240
|
-
serverTrustIdentity:
|
|
234
|
+
droneSession: active.sessionToken,
|
|
235
|
+
apiUrl: active.apiUrl,
|
|
236
|
+
serverTrustIdentity: active.serverTrustIdentity,
|
|
241
237
|
redirect: 'error',
|
|
242
238
|
...(payload === undefined
|
|
243
239
|
? { headers: { Accept: 'application/json' } }
|
|
@@ -246,14 +242,6 @@ async function localServerRequest<T>(
|
|
|
246
242
|
body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
|
|
247
243
|
}),
|
|
248
244
|
}), true);
|
|
249
|
-
try {
|
|
250
|
-
return await request(current);
|
|
251
|
-
} catch (error) {
|
|
252
|
-
if (!(error instanceof BorgServerError) || error.code !== 'AUTH_EXPIRED') throw error;
|
|
253
|
-
if (current.sessionToken !== active.sessionToken) throw error;
|
|
254
|
-
current = await recoverExpiredLocalSession(current);
|
|
255
|
-
return request(current);
|
|
256
|
-
}
|
|
257
245
|
}
|
|
258
246
|
|
|
259
247
|
export interface LocalManageOperation {
|
|
@@ -621,16 +609,16 @@ async function authedFetch(
|
|
|
621
609
|
} catch {
|
|
622
610
|
rejectedCode = undefined;
|
|
623
611
|
}
|
|
624
|
-
if (droneSession !== undefined && rejectedCode === ErrorCode.
|
|
612
|
+
if (droneSession !== undefined && rejectedCode === ErrorCode.SESSION_REJECTED) {
|
|
625
613
|
throw new BorgServerError(
|
|
626
|
-
'
|
|
627
|
-
'the selected Borg server session
|
|
614
|
+
'SESSION_REJECTED',
|
|
615
|
+
'the selected Borg server superseded this worktree session with a newer enrollment',
|
|
628
616
|
);
|
|
629
617
|
}
|
|
630
|
-
if (droneSession !== undefined && rejectedCode === ErrorCode.
|
|
618
|
+
if (droneSession !== undefined && rejectedCode === ErrorCode.SESSION_REVOKED) {
|
|
631
619
|
throw new BorgServerError(
|
|
632
|
-
'
|
|
633
|
-
'the selected Borg server
|
|
620
|
+
'SESSION_REVOKED',
|
|
621
|
+
'the selected Borg server revoked this worktree session',
|
|
634
622
|
);
|
|
635
623
|
}
|
|
636
624
|
throw new BorgServerError(
|
package/src/seat-probe.ts
CHANGED
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
* preserved (CR #6 / CR5 — the probe must NOT collapse them). Each verdict is
|
|
20
20
|
* derived from the actual error TYPE/CODE, never from a mutable error-text regex:
|
|
21
21
|
* evicted ← 410 DRONE_EVICTED (terminal; the SOLE delete authority — gh#882 S1)
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* revoked ← pin-matched drone-SESSION 401 carrying SESSION_REVOKED
|
|
23
|
+
* rejected ← pin-matched drone-SESSION 401 carrying SESSION_REJECTED
|
|
24
|
+
* (superseded by a newer enrollment)
|
|
25
25
|
* credential-rejected← any OTHER 401 on the drone session (bare/untyped or a
|
|
26
26
|
* non-SESSION typed code): the saved credential is no longer
|
|
27
27
|
* accepted, but this is NON-DESTRUCTIVE — re-enroll, NEVER a
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
*/
|
|
41
41
|
export type SeatStatus =
|
|
42
42
|
| 'evicted'
|
|
43
|
+
| 'revoked'
|
|
43
44
|
| 'rejected'
|
|
44
45
|
| 'live'
|
|
45
46
|
| 'credential-rejected'
|
|
@@ -99,6 +100,7 @@ export async function defaultProbeSeat(
|
|
|
99
100
|
// the error TYPE, never from message text (the security boundary).
|
|
100
101
|
if (err instanceof BorgServerTrustError) return 'trust-mismatch';
|
|
101
102
|
if (err instanceof BorgServerError) {
|
|
103
|
+
if (err.code === 'SESSION_REVOKED') return 'revoked';
|
|
102
104
|
if (err.code === 'SESSION_REJECTED') return 'rejected';
|
|
103
105
|
// Every non-SESSION 401 (bare/untyped or a different typed code) is a
|
|
104
106
|
// credential rejection — non-destructive, distinct from a takeover.
|