borgmcp 5.5.0 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +12 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +16 -6
- package/dist/cli-help.js.map +1 -1
- package/dist/docs-sections.js +1 -1
- package/dist/docs-sections.js.map +1 -1
- package/dist/hermes-plugin-install.d.ts +19 -0
- package/dist/hermes-plugin-install.d.ts.map +1 -0
- package/dist/hermes-plugin-install.js +131 -0
- package/dist/hermes-plugin-install.js.map +1 -0
- package/dist/local-server-cursor.d.ts +9 -0
- package/dist/local-server-cursor.d.ts.map +1 -1
- package/dist/local-server-cursor.js +50 -1
- package/dist/local-server-cursor.js.map +1 -1
- package/dist/log-stream.d.ts +13 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +45 -13
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts +3 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +7 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/representative-cmd.d.ts +11 -0
- package/dist/representative-cmd.d.ts.map +1 -1
- package/dist/representative-cmd.js +48 -15
- package/dist/representative-cmd.js.map +1 -1
- package/dist/representative-core.d.ts +53 -7
- package/dist/representative-core.d.ts.map +1 -1
- package/dist/representative-core.js +229 -43
- package/dist/representative-core.js.map +1 -1
- package/dist/representative-delivery-store.d.ts +79 -0
- package/dist/representative-delivery-store.d.ts.map +1 -0
- package/dist/representative-delivery-store.js +233 -0
- package/dist/representative-delivery-store.js.map +1 -0
- package/dist/representative-listener-store.d.ts +46 -0
- package/dist/representative-listener-store.d.ts.map +1 -0
- package/dist/representative-listener-store.js +119 -0
- package/dist/representative-listener-store.js.map +1 -0
- package/dist/representative-listener.d.ts +32 -0
- package/dist/representative-listener.d.ts.map +1 -0
- package/dist/representative-listener.js +293 -0
- package/dist/representative-listener.js.map +1 -0
- package/dist/representative-mcp.d.ts +10 -3
- package/dist/representative-mcp.d.ts.map +1 -1
- package/dist/representative-mcp.js +48 -19
- package/dist/representative-mcp.js.map +1 -1
- package/dist/representative-store.d.ts +7 -0
- package/dist/representative-store.d.ts.map +1 -1
- package/dist/representative-store.js +17 -2
- package/dist/representative-store.js.map +1 -1
- package/dist/seat-probe.d.ts +1 -0
- package/dist/seat-probe.d.ts.map +1 -1
- package/dist/seat-probe.js +1 -1
- package/dist/seat-probe.js.map +1 -1
- package/dist/server-trust.d.ts +10 -0
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +23 -6
- package/dist/server-trust.js.map +1 -1
- package/dist/stream-owner.d.ts.map +1 -1
- package/dist/stream-owner.js +32 -3
- package/dist/stream-owner.js.map +1 -1
- package/docs/HUMAN_REPRESENTATIVE.md +310 -36
- package/hermes-plugin/borg-representative-push/__init__.py +743 -0
- package/hermes-plugin/borg-representative-push/plugin.yaml +39 -0
- package/package.json +3 -1
- package/src/claude.ts +12 -0
- package/src/cli-help.ts +16 -6
- package/src/docs-sections.ts +1 -1
- package/src/hermes-plugin-install.ts +147 -0
- package/src/local-server-cursor.ts +45 -1
- package/src/log-stream.ts +47 -14
- package/src/remote-client.ts +9 -1
- package/src/representative-cmd.ts +52 -20
- package/src/representative-core.ts +255 -46
- package/src/representative-delivery-store.ts +235 -0
- package/src/representative-listener-store.ts +115 -0
- package/src/representative-listener.ts +215 -0
- package/src/representative-mcp.ts +58 -17
- package/src/representative-store.ts +18 -2
- package/src/seat-probe.ts +1 -1
- package/src/server-trust.ts +25 -6
- package/src/stream-owner.ts +34 -2
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Restricted stdio MCP facade for the human representative.
|
|
3
3
|
*
|
|
4
4
|
* A Borg server speaks pinned-TLS HTTPS, not MCP, so a generic MCP host reaches
|
|
5
|
-
* it through this local stdio process. The surface is
|
|
6
|
-
* (to the ONE bound Coordinator), read (that Coordinator's replies)
|
|
5
|
+
* it through this local stdio process. The surface is five tools: status, send
|
|
6
|
+
* (to the ONE bound Coordinator), read (that Coordinator's replies), deliver
|
|
7
|
+
* (the host's durable delivery checkpoint) and ack.
|
|
7
8
|
* There is deliberately no log/broadcast/dispatch, roster-management, grant,
|
|
8
9
|
* evict, release, regen or server-lifecycle tool, and no dispatcher escape hatch.
|
|
9
10
|
*
|
|
@@ -21,18 +22,22 @@ import {
|
|
|
21
22
|
REPRESENTATIVE_MESSAGE_LIMIT_BYTES,
|
|
22
23
|
RepresentativeError,
|
|
23
24
|
ackRepresentativeReply,
|
|
25
|
+
deliverRepresentativeReplies,
|
|
24
26
|
readRepresentativeReplies,
|
|
27
|
+
serializeRepresentativeResult,
|
|
25
28
|
representativeStatus,
|
|
26
29
|
sendRepresentativeMessage,
|
|
27
30
|
type RepresentativeContext,
|
|
28
31
|
} from './representative-core.js';
|
|
29
|
-
import { RepresentativeStoreError } from './representative-store.js';
|
|
32
|
+
import { RepresentativeStoreError, bindingFingerprint } from './representative-store.js';
|
|
33
|
+
import { createDeliveryStore } from './representative-delivery-store.js';
|
|
30
34
|
import { createRepresentativeOwner } from './representative-owner.js';
|
|
31
35
|
|
|
32
36
|
export const REPRESENTATIVE_TOOL_NAMES = [
|
|
33
37
|
'borg_representative-status',
|
|
34
38
|
'borg_representative-send',
|
|
35
39
|
'borg_representative-read',
|
|
40
|
+
'borg_representative-deliver',
|
|
36
41
|
'borg_representative-ack',
|
|
37
42
|
] as const;
|
|
38
43
|
|
|
@@ -83,29 +88,47 @@ const TOOLS = [
|
|
|
83
88
|
{
|
|
84
89
|
name: 'borg_representative-read',
|
|
85
90
|
description:
|
|
86
|
-
'Read
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'is
|
|
91
|
+
'Read the bound Coordinator\'s replies addressed to this representative that come after your delivered checkpoint, ' +
|
|
92
|
+
'oldest first. Reading changes nothing: the same replies return on every call until you call ' +
|
|
93
|
+
'borg_representative-deliver. Persist each reply durably, route it by in_reply_to or hold it for the human, then ' +
|
|
94
|
+
'deliver through the last one you persisted. At most `limit` replies and `max_bytes` of serialized result; a reply ' +
|
|
95
|
+
'larger than max_bytes is returned whole and alone with oversize:true, and the result never exceeds ' +
|
|
96
|
+
'max(max_bytes, 16384) bytes (oversize citations reduced to ids, documents_reduced:true). has_more means more ' +
|
|
97
|
+
'replies follow the window; an empty page never has has_more. ' +
|
|
98
|
+
'Stop routing if binding_fingerprint differs from the value you persisted at binding time.',
|
|
91
99
|
inputSchema: {
|
|
92
100
|
type: 'object',
|
|
93
101
|
properties: {
|
|
94
102
|
include_broadcast: { type: 'boolean', description: 'Also return the Coordinator\'s cube-wide broadcasts (marked as such).' },
|
|
95
|
-
limit: {
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
limit: { type: 'integer', minimum: 1, maximum: 50, description: 'Hard cap on returned replies. Default 10.' },
|
|
104
|
+
max_bytes: {
|
|
105
|
+
type: 'integer', minimum: 4096, maximum: 60000,
|
|
106
|
+
description: 'Hard cap on the serialized result size in bytes. Default 32768.',
|
|
98
107
|
},
|
|
99
108
|
},
|
|
100
109
|
additionalProperties: false,
|
|
101
110
|
},
|
|
102
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: 'borg_representative-deliver',
|
|
114
|
+
description:
|
|
115
|
+
'Record that every reply up to and including `through` is durably persisted by the host. Only this call moves the ' +
|
|
116
|
+
'read window. `through` must be a reply borg_representative-read returned; the same or an older id is a no-op ' +
|
|
117
|
+
'(advanced:false). Call it only after durable persistence: a delivered reply is not returned again. It does not ' +
|
|
118
|
+
'notify the Coordinator; that is borg_representative-ack.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: { through: { type: 'string', description: 'entry_id of the last reply you persisted.' } },
|
|
122
|
+
required: ['through'],
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
103
126
|
{
|
|
104
127
|
name: 'borg_representative-ack',
|
|
105
128
|
description:
|
|
106
129
|
'Signal to the Coordinator that one of its direct replies (entry_id from borg_representative-read) was received. ' +
|
|
107
|
-
'It is only that signal: it
|
|
108
|
-
'not needed for reading.',
|
|
130
|
+
'It is only that signal: it is not delivery, does not move the read window (that is borg_representative-deliver), ' +
|
|
131
|
+
'and is not needed for reading.',
|
|
109
132
|
inputSchema: {
|
|
110
133
|
type: 'object',
|
|
111
134
|
properties: { entry_id: { type: 'string' } },
|
|
@@ -130,13 +153,19 @@ function errorBody(error: unknown): { error: { code: string; message: string; de
|
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
const toolResult = (body: unknown, isError = false) => ({
|
|
133
|
-
content: [{ type: 'text' as const, text:
|
|
156
|
+
content: [{ type: 'text' as const, text: serializeRepresentativeResult(body) }],
|
|
134
157
|
...(isError ? { isError: true } : {}),
|
|
135
158
|
});
|
|
136
159
|
|
|
137
160
|
export interface ServeRepresentativeOptions {
|
|
138
161
|
/** Resolved on every call; a throw fails that call closed. */
|
|
139
162
|
context: () => Promise<RepresentativeContext>;
|
|
163
|
+
/**
|
|
164
|
+
* The binding generation this process started with. Every call except status
|
|
165
|
+
* refuses with BINDING_MISMATCH, before any lease, ledger, checkpoint, cursor
|
|
166
|
+
* or network activity, once the saved binding's fingerprint differs.
|
|
167
|
+
*/
|
|
168
|
+
pinnedFingerprint?: string;
|
|
140
169
|
version: string;
|
|
141
170
|
stdin?: Readable;
|
|
142
171
|
stdout?: Writable;
|
|
@@ -163,16 +192,26 @@ export async function serveRepresentativeMcp(
|
|
|
163
192
|
throw new RepresentativeError(ErrorCode.INVALID_INPUT, `Unknown tool ${JSON.stringify(name)}; this connection exposes only the representative tools.`);
|
|
164
193
|
}
|
|
165
194
|
const ctx = await options.context();
|
|
195
|
+
const pinned = options.pinnedFingerprint ? { pinned_binding_fingerprint: options.pinnedFingerprint } : {};
|
|
166
196
|
if (name === 'borg_representative-status') {
|
|
167
|
-
return toolResult({ ...await representativeStatus(ctx), ownership: await owner.snapshot(ctx.binding) });
|
|
197
|
+
return toolResult({ ...await representativeStatus(ctx), ...pinned, ownership: await owner.snapshot(ctx.binding) });
|
|
198
|
+
}
|
|
199
|
+
if (options.pinnedFingerprint && bindingFingerprint(ctx.binding) !== options.pinnedFingerprint) {
|
|
200
|
+
throw new RepresentativeError(
|
|
201
|
+
'BINDING_MISMATCH',
|
|
202
|
+
'The operator rebound this connection (a new binding generation) while it was running. Restart the MCP server to use the new binding.',
|
|
203
|
+
);
|
|
168
204
|
}
|
|
205
|
+
// An untrustworthy delivery checkpoint stops every effect, not only read
|
|
206
|
+
// and deliver, so the refusal reaches the human. Read-only; status stays.
|
|
207
|
+
await createDeliveryStore(ctx.binding).load();
|
|
169
208
|
await owner.ensure(ctx.binding);
|
|
170
209
|
// Recheck at each network boundary, not just at tool dispatch: a process
|
|
171
210
|
// may have paused or lost its lease while awaiting live verification.
|
|
172
211
|
const backend = ctx.backend;
|
|
173
|
-
const guarded = { ...ctx, backend: Object.fromEntries(['whoami', 'roster', 'append', '
|
|
212
|
+
const guarded = { ...ctx, guard: () => owner.ensure(ctx.binding), backend: Object.fromEntries(['whoami', 'roster', 'append', 'readAfter', 'unreadCursor', 'readEntry', 'ack'].map((key) => [key, async (...args: unknown[]) => {
|
|
174
213
|
await owner.ensure(ctx.binding);
|
|
175
|
-
if (key === '
|
|
214
|
+
if (key === 'readAfter') args[2] = () => owner.ensure(ctx.binding);
|
|
176
215
|
const result = await (backend[key as keyof typeof backend] as (...args: unknown[]) => Promise<unknown>).apply(backend, args);
|
|
177
216
|
await owner.ensure(ctx.binding);
|
|
178
217
|
return result;
|
|
@@ -184,6 +223,8 @@ export async function serveRepresentativeMcp(
|
|
|
184
223
|
}
|
|
185
224
|
case 'borg_representative-read':
|
|
186
225
|
return toolResult(await readRepresentativeReplies(guarded, args));
|
|
226
|
+
case 'borg_representative-deliver':
|
|
227
|
+
return toolResult(await deliverRepresentativeReplies(guarded, args));
|
|
187
228
|
default:
|
|
188
229
|
return toolResult(await ackRepresentativeReply(guarded, args));
|
|
189
230
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { decodeUuid } from 'borgmcp-shared/protocol';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
15
16
|
import { join } from 'node:path';
|
|
16
17
|
import { borgConfigRoot } from './private-root.js';
|
|
17
18
|
import { shellEscape } from './shell-escape.js';
|
|
@@ -22,6 +23,19 @@ export function isRepresentativeUuid(value: unknown): value is string {
|
|
|
22
23
|
}
|
|
23
24
|
const SETTLED_REQUEST_LIMIT = 200;
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Host fence for one binding generation: hex SHA-256 of the canonical JSON array
|
|
28
|
+
* [origin, trustIdentity, cubeId, representativeDroneId, coordinatorDroneId,
|
|
29
|
+
* boundAt]. It changes on every rebind (boundAt) and trust change, and carries
|
|
30
|
+
* no path or credential.
|
|
31
|
+
*/
|
|
32
|
+
export function bindingFingerprint(binding: RepresentativeBinding): string {
|
|
33
|
+
return createHash('sha256').update(JSON.stringify([
|
|
34
|
+
binding.origin, binding.trustIdentity, binding.cubeId,
|
|
35
|
+
binding.representativeDroneId, binding.coordinatorDroneId, binding.boundAt,
|
|
36
|
+
])).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
export interface RepresentativeBinding {
|
|
26
40
|
/** Canonical worktree holding the dedicated representative seat. */
|
|
27
41
|
worktree: string;
|
|
@@ -177,7 +191,9 @@ export function createRepresentativeStore(storePath: string = representativeStor
|
|
|
177
191
|
throw new Error('Refusing to save an invalid representative binding');
|
|
178
192
|
}
|
|
179
193
|
const existing = txn.data.bindings[binding.worktree];
|
|
180
|
-
|
|
194
|
+
// An explicit rebind always starts a new generation (new boundAt, so a new
|
|
195
|
+
// binding_fingerprint), even for the same selection.
|
|
196
|
+
if (existing && sameSelection(existing, binding) && !options.rebind) return 'unchanged' as const;
|
|
181
197
|
if (existing && !options.rebind) {
|
|
182
198
|
throw new RepresentativeStoreError(
|
|
183
199
|
'BINDING_CONFLICT',
|
|
@@ -188,7 +204,7 @@ export function createRepresentativeStore(storePath: string = representativeStor
|
|
|
188
204
|
txn.data.bindings[binding.worktree] = binding;
|
|
189
205
|
// A different selection invalidates the old ledger: its post ids belong to
|
|
190
206
|
// another cube/Coordinator conversation.
|
|
191
|
-
if (existing) delete txn.data.requests[binding.worktree];
|
|
207
|
+
if (existing && !sameSelection(existing, binding)) delete txn.data.requests[binding.worktree];
|
|
192
208
|
await txn.commit();
|
|
193
209
|
return existing ? 'rebound' as const : 'created' as const;
|
|
194
210
|
}),
|
package/src/seat-probe.ts
CHANGED
|
@@ -68,7 +68,7 @@ const TRANSPORT_ERRNOS = new Set([
|
|
|
68
68
|
'ABORT_ERR',
|
|
69
69
|
]);
|
|
70
70
|
|
|
71
|
-
function isTransportFailure(err: unknown): boolean {
|
|
71
|
+
export function isTransportFailure(err: unknown): boolean {
|
|
72
72
|
if (err instanceof BorgServerUnreachableError) return true;
|
|
73
73
|
const e = err as { name?: string; code?: string; cause?: { code?: string } };
|
|
74
74
|
if (e?.name === 'AbortError') return true;
|
package/src/server-trust.ts
CHANGED
|
@@ -457,12 +457,7 @@ export async function loadBorgServerTrust(
|
|
|
457
457
|
let pending = trustCache.get(key);
|
|
458
458
|
if (!pending) {
|
|
459
459
|
pending = (async () => {
|
|
460
|
-
const
|
|
461
|
-
readTrustFile(join(directory, 'ca.crt')),
|
|
462
|
-
readTrustFile(join(directory, 'server.json')),
|
|
463
|
-
]);
|
|
464
|
-
const config = decodeTrustConfig(configText);
|
|
465
|
-
const identity = verifyCaIdentity(certificate, config.ca_spki_sha256);
|
|
460
|
+
const { certificate, identity } = await readLocalAuthorityTrust(directory);
|
|
466
461
|
return { identity, fetchImpl: createPinnedServerFetch(origin, certificate) };
|
|
467
462
|
})();
|
|
468
463
|
trustCache.set(key, pending);
|
|
@@ -494,6 +489,30 @@ export async function loadBorgServerTrust(
|
|
|
494
489
|
});
|
|
495
490
|
}
|
|
496
491
|
|
|
492
|
+
async function readLocalAuthorityTrust(directory: string): Promise<{ certificate: string; identity: string }> {
|
|
493
|
+
const [certificate, configText] = await Promise.all([
|
|
494
|
+
readTrustFile(join(directory, 'ca.crt')),
|
|
495
|
+
readTrustFile(join(directory, 'server.json')),
|
|
496
|
+
]);
|
|
497
|
+
return { certificate, identity: verifyCaIdentity(certificate, decodeTrustConfig(configText).ca_spki_sha256) };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The current trust identity for `origin`, read from disk on every call with the
|
|
502
|
+
* same source selection and file checks as loadBorgServerTrust. Its local-authority
|
|
503
|
+
* branch caches for the process lifetime, which a continuation guard on a
|
|
504
|
+
* long-lived connection must not rely on; the enrollment branch already rereads
|
|
505
|
+
* its pointer per call. It returns the identity only and grants no trust. The
|
|
506
|
+
* local-authority branch builds no fetch; the enrollment delegation may construct
|
|
507
|
+
* the loader's pinned fetch on a cache miss, but this function sends no request.
|
|
508
|
+
*/
|
|
509
|
+
export async function readBorgServerTrustIdentity(origin: string): Promise<string> {
|
|
510
|
+
if (!await remoteTrustStateExists(origin) && await trustFilesExist(serverDataDirectory())) {
|
|
511
|
+
return (await readLocalAuthorityTrust(serverDataDirectory())).identity;
|
|
512
|
+
}
|
|
513
|
+
return (await loadBorgServerTrust(origin)).identity;
|
|
514
|
+
}
|
|
515
|
+
|
|
497
516
|
function pemCertificate(raw: Buffer): string {
|
|
498
517
|
const body = raw.toString('base64').match(/.{1,64}/g)?.join('\n') ?? '';
|
|
499
518
|
return `-----BEGIN CERTIFICATE-----\n${body}\n-----END CERTIFICATE-----\n`;
|
package/src/stream-owner.ts
CHANGED
|
@@ -139,8 +139,37 @@ export async function readOwnershipSnapshot(
|
|
|
139
139
|
deps: StreamOwnerDeps = {}
|
|
140
140
|
): Promise<StreamOwnershipSnapshot> {
|
|
141
141
|
const lockPath = streamLockPath(cubeId, droneId, deps.locksDir);
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// A refresh renames the lock to `<lock>.takeover` while it rewrites the record
|
|
143
|
+
// and then restores it. Read-only: report an intact, fresh, live in-flight
|
|
144
|
+
// owner there; a stale, future-dated, dead or malformed leftover stays
|
|
145
|
+
// `unowned`. A missing directory or owner leaf is re-resolved from the lock
|
|
146
|
+
// path, so one refresh overlapping any step cannot hide the owner; genuine
|
|
147
|
+
// initialization still reports `initializing` after the bounded attempts.
|
|
148
|
+
let initializing: StreamOwnershipSnapshot | undefined;
|
|
149
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
150
|
+
const inspected = await readBoundOwner(lockPath, deps);
|
|
151
|
+
if (inspected) {
|
|
152
|
+
const snapshot = snapshotFromOwner(lockPath, inspected, deps);
|
|
153
|
+
if (inspected.raw !== null) return snapshot;
|
|
154
|
+
initializing = snapshot;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const claimed = await readBoundOwner(takeoverPath(lockPath), deps);
|
|
158
|
+
if (!claimed || claimed.raw === null) continue;
|
|
159
|
+
const snapshot = snapshotFromOwner(lockPath, claimed, deps);
|
|
160
|
+
const ageMs = snapshot.ageMs ?? Infinity;
|
|
161
|
+
const live = snapshot.pid !== undefined && ageMs >= 0 && ageMs <= STREAM_OWNER_STALE_MS &&
|
|
162
|
+
isPidAlive(snapshot.pid, deps);
|
|
163
|
+
return live ? snapshot : { state: 'unowned', lockPath };
|
|
164
|
+
}
|
|
165
|
+
return initializing ?? { state: 'unowned', lockPath };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function snapshotFromOwner(
|
|
169
|
+
lockPath: string,
|
|
170
|
+
inspected: { raw: string | null; stat: { dev: number; ino: number; mtimeMs: number } },
|
|
171
|
+
deps: StreamOwnerDeps,
|
|
172
|
+
): StreamOwnershipSnapshot {
|
|
144
173
|
const { raw, stat: lockStat } = inspected;
|
|
145
174
|
if (raw === null) {
|
|
146
175
|
const now = (deps.now ?? (() => new Date()))();
|
|
@@ -666,6 +695,9 @@ async function readBoundOwner(lockPath: string, deps: StreamOwnerDeps = {}): Pro
|
|
|
666
695
|
? await readStoreFile(path.join(lockPath, OWNER_FILE), { secureRoot: lockPath, createRoot: false })
|
|
667
696
|
: await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
|
|
668
697
|
} catch (error: any) {
|
|
698
|
+
// A concurrent refresh atomically replaces the owner file; like other
|
|
699
|
+
// lock inspection, that identity drift is turnover and is re-read.
|
|
700
|
+
if (error?.code === 'STORE_FILE_IDENTITY_CHANGED') continue;
|
|
669
701
|
if (error?.code !== 'ENOENT') throw error;
|
|
670
702
|
raw = null;
|
|
671
703
|
}
|