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.
Files changed (72) hide show
  1. package/README.md +12 -0
  2. package/dist/assimilate-cmd.d.ts +8 -1
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +54 -21
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/claude.d.ts.map +1 -1
  7. package/dist/claude.js +22 -0
  8. package/dist/claude.js.map +1 -1
  9. package/dist/cli-help.d.ts +1 -0
  10. package/dist/cli-help.d.ts.map +1 -1
  11. package/dist/cli-help.js +42 -0
  12. package/dist/cli-help.js.map +1 -1
  13. package/dist/docs-sections.d.ts.map +1 -1
  14. package/dist/docs-sections.js +8 -0
  15. package/dist/docs-sections.js.map +1 -1
  16. package/dist/local-server-cursor.d.ts +1 -1
  17. package/dist/local-server-cursor.d.ts.map +1 -1
  18. package/dist/local-server-cursor.js +14 -4
  19. package/dist/local-server-cursor.js.map +1 -1
  20. package/dist/remote-client.d.ts +14 -0
  21. package/dist/remote-client.d.ts.map +1 -1
  22. package/dist/remote-client.js +30 -14
  23. package/dist/remote-client.js.map +1 -1
  24. package/dist/representative-cmd.d.ts +87 -0
  25. package/dist/representative-cmd.d.ts.map +1 -0
  26. package/dist/representative-cmd.js +286 -0
  27. package/dist/representative-cmd.js.map +1 -0
  28. package/dist/representative-core.d.ts +197 -0
  29. package/dist/representative-core.d.ts.map +1 -0
  30. package/dist/representative-core.js +493 -0
  31. package/dist/representative-core.js.map +1 -0
  32. package/dist/representative-mcp.d.ts +30 -0
  33. package/dist/representative-mcp.d.ts.map +1 -0
  34. package/dist/representative-mcp.js +182 -0
  35. package/dist/representative-mcp.js.map +1 -0
  36. package/dist/representative-owner.d.ts +10 -0
  37. package/dist/representative-owner.d.ts.map +1 -0
  38. package/dist/representative-owner.js +107 -0
  39. package/dist/representative-owner.js.map +1 -0
  40. package/dist/representative-store.d.ts +61 -0
  41. package/dist/representative-store.d.ts.map +1 -0
  42. package/dist/representative-store.js +158 -0
  43. package/dist/representative-store.js.map +1 -0
  44. package/dist/seat-store.d.ts +13 -0
  45. package/dist/seat-store.d.ts.map +1 -1
  46. package/dist/seat-store.js +55 -10
  47. package/dist/seat-store.js.map +1 -1
  48. package/dist/stream-owner.d.ts +10 -0
  49. package/dist/stream-owner.d.ts.map +1 -1
  50. package/dist/stream-owner.js +98 -19
  51. package/dist/stream-owner.js.map +1 -1
  52. package/dist/unknown-subcommand.d.ts +1 -1
  53. package/dist/unknown-subcommand.d.ts.map +1 -1
  54. package/dist/unknown-subcommand.js +1 -0
  55. package/dist/unknown-subcommand.js.map +1 -1
  56. package/docs/HUMAN_REPRESENTATIVE.md +269 -0
  57. package/docs/RELEASING.md +2 -2
  58. package/package.json +2 -2
  59. package/src/assimilate-cmd.ts +73 -22
  60. package/src/claude.ts +22 -0
  61. package/src/cli-help.ts +45 -0
  62. package/src/docs-sections.ts +8 -0
  63. package/src/local-server-cursor.ts +11 -3
  64. package/src/remote-client.ts +45 -12
  65. package/src/representative-cmd.ts +363 -0
  66. package/src/representative-core.ts +699 -0
  67. package/src/representative-mcp.ts +209 -0
  68. package/src/representative-owner.ts +105 -0
  69. package/src/representative-store.ts +208 -0
  70. package/src/seat-store.ts +61 -10
  71. package/src/stream-owner.ts +96 -19
  72. package/src/unknown-subcommand.ts +1 -0
@@ -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 fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
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 (deps.writeRecord ?? writeRecord)(lockPath, record);
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 fs.writeFile(
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(await fs.readFile(path.join(claimPath, TAKEOVER_FILE), 'utf8'));
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 (deps.writeRecord ?? writeRecord)(claimPath, next);
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 fs.writeFile(
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 = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
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`);
@@ -27,6 +27,7 @@ export const KNOWN_SUBCOMMANDS = [
27
27
  'drones',
28
28
  'launch',
29
29
  'launch-all',
30
+ 'representative',
30
31
  'server',
31
32
  ] as const;
32
33