borgmcp 5.4.0 → 5.5.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 +12 -0
- package/dist/assimilate-cmd.d.ts +8 -1
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +54 -21
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +22 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +42 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/docs-sections.d.ts.map +1 -1
- package/dist/docs-sections.js +8 -0
- package/dist/docs-sections.js.map +1 -1
- package/dist/local-server-cursor.d.ts +1 -1
- package/dist/local-server-cursor.d.ts.map +1 -1
- package/dist/local-server-cursor.js +14 -4
- package/dist/local-server-cursor.js.map +1 -1
- package/dist/remote-client.d.ts +14 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +30 -14
- package/dist/remote-client.js.map +1 -1
- package/dist/representative-cmd.d.ts +87 -0
- package/dist/representative-cmd.d.ts.map +1 -0
- package/dist/representative-cmd.js +286 -0
- package/dist/representative-cmd.js.map +1 -0
- package/dist/representative-core.d.ts +197 -0
- package/dist/representative-core.d.ts.map +1 -0
- package/dist/representative-core.js +493 -0
- package/dist/representative-core.js.map +1 -0
- package/dist/representative-mcp.d.ts +30 -0
- package/dist/representative-mcp.d.ts.map +1 -0
- package/dist/representative-mcp.js +182 -0
- package/dist/representative-mcp.js.map +1 -0
- package/dist/representative-owner.d.ts +10 -0
- package/dist/representative-owner.d.ts.map +1 -0
- package/dist/representative-owner.js +107 -0
- package/dist/representative-owner.js.map +1 -0
- package/dist/representative-store.d.ts +61 -0
- package/dist/representative-store.d.ts.map +1 -0
- package/dist/representative-store.js +158 -0
- package/dist/representative-store.js.map +1 -0
- package/dist/seat-store.d.ts +13 -0
- package/dist/seat-store.d.ts.map +1 -1
- package/dist/seat-store.js +55 -10
- package/dist/seat-store.js.map +1 -1
- package/dist/stream-owner.d.ts +10 -0
- package/dist/stream-owner.d.ts.map +1 -1
- package/dist/stream-owner.js +98 -19
- package/dist/stream-owner.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/HUMAN_REPRESENTATIVE.md +269 -0
- package/docs/RELEASING.md +2 -2
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +73 -22
- package/src/claude.ts +22 -0
- package/src/cli-help.ts +45 -0
- package/src/docs-sections.ts +8 -0
- package/src/local-server-cursor.ts +11 -3
- package/src/remote-client.ts +45 -12
- package/src/representative-cmd.ts +363 -0
- package/src/representative-core.ts +699 -0
- package/src/representative-mcp.ts +209 -0
- package/src/representative-owner.ts +105 -0
- package/src/representative-store.ts +208 -0
- package/src/seat-store.ts +61 -10
- package/src/stream-owner.ts +96 -19
- package/src/unknown-subcommand.ts +1 -0
package/src/stream-owner.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { borgConfigRoot } from './private-root.js';
|
|
5
|
+
import { assertSecureRoot, atomicWrite0600, readStoreFile } from './seat-store.js';
|
|
5
6
|
|
|
6
7
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
8
|
const STREAM_LOCKS_DIR = path.join(borgConfigRoot(), 'stream-locks');
|
|
@@ -58,6 +59,13 @@ export interface StreamOwnerDeps {
|
|
|
58
59
|
pid?: number;
|
|
59
60
|
cwd?: string;
|
|
60
61
|
locksDir?: string;
|
|
62
|
+
/** Production opt-in for private lease storage beneath this canonical root.
|
|
63
|
+
* Ancestors from boundary to root use the store owner-controlled policy;
|
|
64
|
+
* root and descendants require 0700. Uses private reads and exclusive,
|
|
65
|
+
* identity-checked leaf publication. Absent: ordinary stream I/O unchanged.
|
|
66
|
+
* Static hostile objects are in scope; same-UID syscall races are not.
|
|
67
|
+
*/
|
|
68
|
+
privateRoot?: { root: string; boundary: string };
|
|
61
69
|
processNonce?: string;
|
|
62
70
|
processStartedAt?: string;
|
|
63
71
|
worktree?: string;
|
|
@@ -88,7 +96,8 @@ export async function acquireStreamLease(
|
|
|
88
96
|
deps: StreamOwnerDeps = {}
|
|
89
97
|
): Promise<StreamLease | null> {
|
|
90
98
|
const lockPath = streamLockPath(cubeId, droneId, deps.locksDir);
|
|
91
|
-
await
|
|
99
|
+
if (deps.privateRoot) await privateLeaseDirectory(path.dirname(lockPath), deps, true);
|
|
100
|
+
else await fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
92
101
|
|
|
93
102
|
if (!(await recoverTakeoverClaim(lockPath, staleMs, deps))) return null;
|
|
94
103
|
|
|
@@ -130,7 +139,7 @@ export async function readOwnershipSnapshot(
|
|
|
130
139
|
deps: StreamOwnerDeps = {}
|
|
131
140
|
): Promise<StreamOwnershipSnapshot> {
|
|
132
141
|
const lockPath = streamLockPath(cubeId, droneId, deps.locksDir);
|
|
133
|
-
const inspected = await readBoundOwner(lockPath);
|
|
142
|
+
const inspected = await readBoundOwner(lockPath, deps);
|
|
134
143
|
if (!inspected) return { state: 'unowned', lockPath };
|
|
135
144
|
const { raw, stat: lockStat } = inspected;
|
|
136
145
|
if (raw === null) {
|
|
@@ -200,6 +209,10 @@ async function tryCreateLease(
|
|
|
200
209
|
lockPath: string,
|
|
201
210
|
deps: StreamOwnerDeps
|
|
202
211
|
): Promise<StreamLease | null> {
|
|
212
|
+
if (deps.privateRoot) {
|
|
213
|
+
await privateLeaseDirectory(lockPath, deps, false);
|
|
214
|
+
await privateLeaseDirectory(takeoverPath(lockPath), deps, false);
|
|
215
|
+
}
|
|
203
216
|
if (await pathExists(takeoverPath(lockPath))) return null;
|
|
204
217
|
try {
|
|
205
218
|
await fs.mkdir(lockPath, { mode: 0o700 });
|
|
@@ -210,22 +223,22 @@ async function tryCreateLease(
|
|
|
210
223
|
|
|
211
224
|
const record = makeRecord(deps);
|
|
212
225
|
try {
|
|
213
|
-
await (
|
|
226
|
+
await writeLeaseRecord(lockPath, record, deps);
|
|
214
227
|
if (await pathExists(takeoverPath(lockPath))) {
|
|
215
|
-
await cleanupFailedInitialization(lockPath, record).catch(() => {});
|
|
228
|
+
await cleanupFailedInitialization(lockPath, record, deps).catch(() => {});
|
|
216
229
|
return null;
|
|
217
230
|
}
|
|
218
231
|
} catch (err) {
|
|
219
|
-
await cleanupFailedInitialization(lockPath, record).catch(() => {});
|
|
232
|
+
await cleanupFailedInitialization(lockPath, record, deps).catch(() => {});
|
|
220
233
|
throw err;
|
|
221
234
|
}
|
|
222
|
-
const created = await readBoundOwner(lockPath);
|
|
235
|
+
const created = await readBoundOwner(lockPath, deps);
|
|
223
236
|
const createdRecord = created?.raw ? parseOwnershipRecord(created.raw) : null;
|
|
224
237
|
if (
|
|
225
238
|
!created || !createdRecord ||
|
|
226
239
|
!sameOwner(record, createdRecord)
|
|
227
240
|
) {
|
|
228
|
-
await cleanupFailedInitialization(lockPath, record).catch(() => {});
|
|
241
|
+
await cleanupFailedInitialization(lockPath, record, deps).catch(() => {});
|
|
229
242
|
throw new Error('Borg stream lease initialization lost ownership');
|
|
230
243
|
}
|
|
231
244
|
return makeLease(
|
|
@@ -243,7 +256,8 @@ async function tryCreateLease(
|
|
|
243
256
|
*/
|
|
244
257
|
async function cleanupFailedInitialization(
|
|
245
258
|
lockPath: string,
|
|
246
|
-
attempted: StreamOwnerRecord
|
|
259
|
+
attempted: StreamOwnerRecord,
|
|
260
|
+
deps: StreamOwnerDeps,
|
|
247
261
|
): Promise<void> {
|
|
248
262
|
const cleanupPath = `${lockPath}.failed-${attempted.processNonce}-${Date.now()}`;
|
|
249
263
|
try {
|
|
@@ -252,7 +266,7 @@ async function cleanupFailedInitialization(
|
|
|
252
266
|
if (err?.code === 'ENOENT') return;
|
|
253
267
|
throw err;
|
|
254
268
|
}
|
|
255
|
-
const current = await readOwnershipRecord(cleanupPath);
|
|
269
|
+
const current = await readOwnershipRecord(cleanupPath, deps);
|
|
256
270
|
if (current && (current.pid !== attempted.pid || current.processNonce !== attempted.processNonce)) {
|
|
257
271
|
try {
|
|
258
272
|
await fs.rename(cleanupPath, lockPath);
|
|
@@ -274,6 +288,10 @@ async function moveStaleLockAside(
|
|
|
274
288
|
deps: StreamOwnerDeps
|
|
275
289
|
): Promise<boolean> {
|
|
276
290
|
const claimPath = takeoverPath(lockPath);
|
|
291
|
+
if (deps.privateRoot) {
|
|
292
|
+
await privateLeaseDirectory(lockPath, deps, false);
|
|
293
|
+
await privateLeaseDirectory(claimPath, deps, false);
|
|
294
|
+
}
|
|
277
295
|
try {
|
|
278
296
|
await fs.rename(lockPath, claimPath);
|
|
279
297
|
} catch (err: any) {
|
|
@@ -301,13 +319,14 @@ async function moveStaleLockAside(
|
|
|
301
319
|
processNonce: deps.processNonce ?? processNonce,
|
|
302
320
|
claimedAt: (deps.now ?? (() => new Date()))().toISOString(),
|
|
303
321
|
};
|
|
304
|
-
await
|
|
322
|
+
if (deps.privateRoot) await writePrivateLeaseFile(claimPath, TAKEOVER_FILE, claimant, deps, true);
|
|
323
|
+
else await fs.writeFile(
|
|
305
324
|
path.join(claimPath, TAKEOVER_FILE),
|
|
306
325
|
JSON.stringify(claimant, null, 2) + '\n',
|
|
307
326
|
{ flag: 'wx', mode: 0o600 },
|
|
308
327
|
);
|
|
309
328
|
await deps.beforeTakeoverVerify?.(claimPath);
|
|
310
|
-
const verified = await readOwnershipRecord(claimPath);
|
|
329
|
+
const verified = await readOwnershipRecord(claimPath, deps);
|
|
311
330
|
const verifiedStat = await fs.stat(claimPath).catch(() => null);
|
|
312
331
|
if (!isStillReclaimable(snapshot, verified, verifiedStat, staleMs, deps)) {
|
|
313
332
|
await fs.unlink(path.join(claimPath, TAKEOVER_FILE)).catch(() => {});
|
|
@@ -344,6 +363,10 @@ async function recoverTakeoverClaim(
|
|
|
344
363
|
deps: StreamOwnerDeps,
|
|
345
364
|
): Promise<boolean> {
|
|
346
365
|
const claimPath = takeoverPath(lockPath);
|
|
366
|
+
if (deps.privateRoot) {
|
|
367
|
+
await privateLeaseDirectory(lockPath, deps, false);
|
|
368
|
+
await privateLeaseDirectory(claimPath, deps, false);
|
|
369
|
+
}
|
|
347
370
|
let claimStat;
|
|
348
371
|
try {
|
|
349
372
|
claimStat = await fs.stat(claimPath);
|
|
@@ -353,9 +376,14 @@ async function recoverTakeoverClaim(
|
|
|
353
376
|
}
|
|
354
377
|
|
|
355
378
|
const now = (deps.now ?? (() => new Date()))();
|
|
379
|
+
const privateClaim = deps.privateRoot
|
|
380
|
+
? await readStoreFile(path.join(claimPath, TAKEOVER_FILE), { secureRoot: claimPath, createRoot: false })
|
|
381
|
+
: undefined;
|
|
356
382
|
let claimant: { pid: number; processNonce: string; claimedAt: string } | null = null;
|
|
357
383
|
try {
|
|
358
|
-
const parsed = JSON.parse(
|
|
384
|
+
const parsed = JSON.parse((deps.privateRoot
|
|
385
|
+
? privateClaim
|
|
386
|
+
: await fs.readFile(path.join(claimPath, TAKEOVER_FILE), 'utf8')) ?? 'null');
|
|
359
387
|
if (
|
|
360
388
|
parsed?.schemaVersion === 1 &&
|
|
361
389
|
Number.isSafeInteger(parsed.pid) && parsed.pid > 0 &&
|
|
@@ -377,7 +405,7 @@ async function recoverTakeoverClaim(
|
|
|
377
405
|
) return false;
|
|
378
406
|
if (!claimant && claimAge <= STREAM_OWNER_TAKEOVER_STALE_MS) return false;
|
|
379
407
|
|
|
380
|
-
const owner = await readOwnershipRecord(claimPath);
|
|
408
|
+
const owner = await readOwnershipRecord(claimPath, deps);
|
|
381
409
|
const ownerHeartbeat = owner ? Date.parse(owner.heartbeatAt) : Number.NaN;
|
|
382
410
|
const ownerAge = Number.isFinite(ownerHeartbeat)
|
|
383
411
|
? now.getTime() - ownerHeartbeat
|
|
@@ -481,7 +509,7 @@ function makeLease(
|
|
|
481
509
|
ownedRecord,
|
|
482
510
|
deps,
|
|
483
511
|
async (claimPath) => {
|
|
484
|
-
await (
|
|
512
|
+
await writeLeaseRecord(claimPath, next, deps);
|
|
485
513
|
return 'restore';
|
|
486
514
|
},
|
|
487
515
|
);
|
|
@@ -511,6 +539,10 @@ async function mutateClaimedLease(
|
|
|
511
539
|
mutation: (claimPath: string) => Promise<'restore' | 'remove'>,
|
|
512
540
|
): Promise<boolean> {
|
|
513
541
|
const claimPath = takeoverPath(lockPath);
|
|
542
|
+
if (deps.privateRoot) {
|
|
543
|
+
await privateLeaseDirectory(lockPath, deps, false);
|
|
544
|
+
await privateLeaseDirectory(claimPath, deps, false);
|
|
545
|
+
}
|
|
514
546
|
try {
|
|
515
547
|
await fs.rename(lockPath, claimPath);
|
|
516
548
|
} catch (error: any) {
|
|
@@ -532,13 +564,14 @@ async function mutateClaimedLease(
|
|
|
532
564
|
processNonce: deps.processNonce ?? processNonce,
|
|
533
565
|
claimedAt: (deps.now ?? (() => new Date()))().toISOString(),
|
|
534
566
|
};
|
|
535
|
-
await
|
|
567
|
+
if (deps.privateRoot) await writePrivateLeaseFile(claimPath, TAKEOVER_FILE, claimant, deps, true);
|
|
568
|
+
else await fs.writeFile(
|
|
536
569
|
path.join(claimPath, TAKEOVER_FILE),
|
|
537
570
|
JSON.stringify(claimant, null, 2) + '\n',
|
|
538
571
|
{ flag: 'wx', mode: 0o600 },
|
|
539
572
|
);
|
|
540
573
|
|
|
541
|
-
const inspected = await readBoundOwner(claimPath);
|
|
574
|
+
const inspected = await readBoundOwner(claimPath, deps);
|
|
542
575
|
const current = inspected?.raw ? parseOwnershipRecord(inspected.raw) : null;
|
|
543
576
|
if (
|
|
544
577
|
!inspected || !sameIdentity(identity, inspected.stat) || !current ||
|
|
@@ -590,7 +623,11 @@ function sameOwner(left: StreamOwnerRecord, right: StreamOwnerRecord): boolean {
|
|
|
590
623
|
left.cubeName === right.cubeName;
|
|
591
624
|
}
|
|
592
625
|
|
|
593
|
-
async function readOwnershipRecord(lockPath: string): Promise<StreamOwnerRecord | null> {
|
|
626
|
+
async function readOwnershipRecord(lockPath: string, deps: StreamOwnerDeps = {}): Promise<StreamOwnerRecord | null> {
|
|
627
|
+
if (deps.privateRoot) {
|
|
628
|
+
const inspected = await readBoundOwner(lockPath, deps);
|
|
629
|
+
return inspected?.raw ? parseOwnershipRecord(inspected.raw) : null;
|
|
630
|
+
}
|
|
594
631
|
try {
|
|
595
632
|
const raw = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
|
|
596
633
|
return parseOwnershipRecord(raw);
|
|
@@ -608,10 +645,11 @@ function parseOwnershipRecord(raw: string): StreamOwnerRecord | null {
|
|
|
608
645
|
}
|
|
609
646
|
}
|
|
610
647
|
|
|
611
|
-
async function readBoundOwner(lockPath: string): Promise<{
|
|
648
|
+
async function readBoundOwner(lockPath: string, deps: StreamOwnerDeps = {}): Promise<{
|
|
612
649
|
raw: string | null;
|
|
613
650
|
stat: { dev: number; ino: number; mtimeMs: number };
|
|
614
651
|
} | null> {
|
|
652
|
+
if (deps.privateRoot && !await privateLeaseDirectory(lockPath, deps, false)) return null;
|
|
615
653
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
616
654
|
let handle;
|
|
617
655
|
try {
|
|
@@ -624,7 +662,9 @@ async function readBoundOwner(lockPath: string): Promise<{
|
|
|
624
662
|
const openedStat = await handle.stat();
|
|
625
663
|
let raw: string | null;
|
|
626
664
|
try {
|
|
627
|
-
raw =
|
|
665
|
+
raw = deps.privateRoot
|
|
666
|
+
? await readStoreFile(path.join(lockPath, OWNER_FILE), { secureRoot: lockPath, createRoot: false })
|
|
667
|
+
: await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
|
|
628
668
|
} catch (error: any) {
|
|
629
669
|
if (error?.code !== 'ENOENT') throw error;
|
|
630
670
|
raw = null;
|
|
@@ -652,6 +692,43 @@ function sameIdentity(
|
|
|
652
692
|
return left.dev === right.dev && left.ino === right.ino;
|
|
653
693
|
}
|
|
654
694
|
|
|
695
|
+
/** Apply the existing private-store directory policy without creating on reads. */
|
|
696
|
+
async function privateLeaseDirectory(directory: string, deps: StreamOwnerDeps, create: boolean): Promise<boolean> {
|
|
697
|
+
const { root, boundary } = deps.privateRoot!;
|
|
698
|
+
const ancestors = path.relative(boundary, root);
|
|
699
|
+
if (ancestors.startsWith('..') || path.isAbsolute(ancestors)) {
|
|
700
|
+
throw new Error('Borg private lease root escapes its trusted boundary');
|
|
701
|
+
}
|
|
702
|
+
const relative = path.relative(root, directory);
|
|
703
|
+
if (path.resolve(directory) !== directory || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
704
|
+
throw new Error('Borg private lease path escapes its private root');
|
|
705
|
+
}
|
|
706
|
+
let ancestor = boundary;
|
|
707
|
+
for (const component of ancestors.split(path.sep).filter(Boolean)) {
|
|
708
|
+
if (!await assertSecureRoot(ancestor, 'owner-controlled', create)) return false;
|
|
709
|
+
ancestor = path.join(ancestor, component);
|
|
710
|
+
}
|
|
711
|
+
if (!await assertSecureRoot(root, 'private', create)) return false;
|
|
712
|
+
let current = root;
|
|
713
|
+
for (const component of relative.split(path.sep).filter(Boolean)) {
|
|
714
|
+
current = path.join(current, component);
|
|
715
|
+
if (!await assertSecureRoot(current, 'private', create)) return false;
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function writePrivateLeaseFile(directory: string, name: string, value: unknown, deps: StreamOwnerDeps, exclusive = false): Promise<void> {
|
|
721
|
+
if (!await privateLeaseDirectory(directory, deps, false)) throw new Error('Borg private lease directory disappeared');
|
|
722
|
+
await atomicWrite0600(path.join(directory, name), JSON.stringify(value, null, 2) + '\n', {
|
|
723
|
+
secureRoot: directory, verifyLeafIdentity: true, exclusive, createRoot: false,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function writeLeaseRecord(directory: string, record: StreamOwnerRecord, deps: StreamOwnerDeps): Promise<void> {
|
|
728
|
+
if (deps.privateRoot) await writePrivateLeaseFile(directory, OWNER_FILE, record, deps);
|
|
729
|
+
else await (deps.writeRecord ?? writeRecord)(directory, record);
|
|
730
|
+
}
|
|
731
|
+
|
|
655
732
|
async function writeRecord(lockPath: string, record: StreamOwnerRecord): Promise<void> {
|
|
656
733
|
const ownerPath = path.join(lockPath, OWNER_FILE);
|
|
657
734
|
const tmpPath = path.join(lockPath, `${OWNER_FILE}.${record.processNonce}.tmp`);
|