borgmcp 5.4.1 → 5.6.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 (101) hide show
  1. package/README.md +15 -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 +30 -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 +45 -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 +10 -1
  17. package/dist/local-server-cursor.d.ts.map +1 -1
  18. package/dist/local-server-cursor.js +64 -5
  19. package/dist/local-server-cursor.js.map +1 -1
  20. package/dist/log-stream.d.ts +13 -0
  21. package/dist/log-stream.d.ts.map +1 -1
  22. package/dist/log-stream.js +45 -13
  23. package/dist/log-stream.js.map +1 -1
  24. package/dist/remote-client.d.ts +17 -0
  25. package/dist/remote-client.d.ts.map +1 -1
  26. package/dist/remote-client.js +37 -15
  27. package/dist/remote-client.js.map +1 -1
  28. package/dist/representative-cmd.d.ts +94 -0
  29. package/dist/representative-cmd.d.ts.map +1 -0
  30. package/dist/representative-cmd.js +290 -0
  31. package/dist/representative-cmd.js.map +1 -0
  32. package/dist/representative-core.d.ts +243 -0
  33. package/dist/representative-core.d.ts.map +1 -0
  34. package/dist/representative-core.js +679 -0
  35. package/dist/representative-core.js.map +1 -0
  36. package/dist/representative-delivery-store.d.ts +79 -0
  37. package/dist/representative-delivery-store.d.ts.map +1 -0
  38. package/dist/representative-delivery-store.js +233 -0
  39. package/dist/representative-delivery-store.js.map +1 -0
  40. package/dist/representative-listener-store.d.ts +46 -0
  41. package/dist/representative-listener-store.d.ts.map +1 -0
  42. package/dist/representative-listener-store.js +119 -0
  43. package/dist/representative-listener-store.js.map +1 -0
  44. package/dist/representative-listener.d.ts +32 -0
  45. package/dist/representative-listener.d.ts.map +1 -0
  46. package/dist/representative-listener.js +293 -0
  47. package/dist/representative-listener.js.map +1 -0
  48. package/dist/representative-mcp.d.ts +37 -0
  49. package/dist/representative-mcp.d.ts.map +1 -0
  50. package/dist/representative-mcp.js +211 -0
  51. package/dist/representative-mcp.js.map +1 -0
  52. package/dist/representative-owner.d.ts +10 -0
  53. package/dist/representative-owner.d.ts.map +1 -0
  54. package/dist/representative-owner.js +107 -0
  55. package/dist/representative-owner.js.map +1 -0
  56. package/dist/representative-store.d.ts +68 -0
  57. package/dist/representative-store.d.ts.map +1 -0
  58. package/dist/representative-store.js +173 -0
  59. package/dist/representative-store.js.map +1 -0
  60. package/dist/seat-probe.d.ts +1 -0
  61. package/dist/seat-probe.d.ts.map +1 -1
  62. package/dist/seat-probe.js +1 -1
  63. package/dist/seat-probe.js.map +1 -1
  64. package/dist/seat-store.d.ts +13 -0
  65. package/dist/seat-store.d.ts.map +1 -1
  66. package/dist/seat-store.js +55 -10
  67. package/dist/seat-store.js.map +1 -1
  68. package/dist/server-trust.d.ts +10 -0
  69. package/dist/server-trust.d.ts.map +1 -1
  70. package/dist/server-trust.js +23 -6
  71. package/dist/server-trust.js.map +1 -1
  72. package/dist/stream-owner.d.ts +10 -0
  73. package/dist/stream-owner.d.ts.map +1 -1
  74. package/dist/stream-owner.js +129 -21
  75. package/dist/stream-owner.js.map +1 -1
  76. package/dist/unknown-subcommand.d.ts +1 -1
  77. package/dist/unknown-subcommand.d.ts.map +1 -1
  78. package/dist/unknown-subcommand.js +1 -0
  79. package/dist/unknown-subcommand.js.map +1 -1
  80. package/docs/HUMAN_REPRESENTATIVE.md +434 -0
  81. package/package.json +1 -1
  82. package/src/assimilate-cmd.ts +73 -22
  83. package/src/claude.ts +30 -0
  84. package/src/cli-help.ts +48 -0
  85. package/src/docs-sections.ts +8 -0
  86. package/src/local-server-cursor.ts +56 -4
  87. package/src/log-stream.ts +47 -14
  88. package/src/remote-client.ts +54 -13
  89. package/src/representative-cmd.ts +369 -0
  90. package/src/representative-core.ts +908 -0
  91. package/src/representative-delivery-store.ts +235 -0
  92. package/src/representative-listener-store.ts +115 -0
  93. package/src/representative-listener.ts +215 -0
  94. package/src/representative-mcp.ts +250 -0
  95. package/src/representative-owner.ts +105 -0
  96. package/src/representative-store.ts +224 -0
  97. package/src/seat-probe.ts +1 -1
  98. package/src/seat-store.ts +61 -10
  99. package/src/server-trust.ts +25 -6
  100. package/src/stream-owner.ts +129 -20
  101. package/src/unknown-subcommand.ts +1 -0
package/src/seat-store.ts CHANGED
@@ -49,16 +49,24 @@ interface LockPayload {
49
49
  export interface SecureStoreOptions {
50
50
  secureRoot?: string;
51
51
  rootMode?: 'private' | 'owner-controlled';
52
+ /** Harden leaf publication for private lease records; default store behavior is unchanged. */
53
+ verifyLeafIdentity?: boolean;
54
+ /** Publish without replacing an existing leaf (lease takeover marker). */
55
+ exclusive?: boolean;
56
+ /** Read-only lease inspection must not recreate a directory moved by its owner. */
57
+ createRoot?: boolean;
52
58
  }
53
59
 
54
60
  function expectedUid(): number | null {
55
61
  return typeof process.getuid === 'function' ? process.getuid() : null;
56
62
  }
57
63
 
58
- async function assertSecureRoot(
64
+ /** Shared directory policy; create=false validates without mutating and returns false if absent. */
65
+ export async function assertSecureRoot(
59
66
  root: string,
60
67
  rootMode: SecureStoreOptions['rootMode'] = 'private',
61
- ): Promise<void> {
68
+ create = true,
69
+ ): Promise<boolean> {
62
70
  if (!isCanonicalPath(root)) {
63
71
  throw new Error(`Borg credential store path ${root} is not canonical`);
64
72
  }
@@ -67,6 +75,7 @@ async function assertSecureRoot(
67
75
  metadata = await lstat(root);
68
76
  } catch (error) {
69
77
  if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
78
+ if (!create) return false;
70
79
  await mkdir(root, { recursive: true, mode: 0o700 });
71
80
  metadata = await lstat(root);
72
81
  }
@@ -87,6 +96,7 @@ async function assertSecureRoot(
87
96
  if (!isCanonicalPath(root)) {
88
97
  throw new Error(`Borg credential store root ${root} is not canonical or contains a symlink`);
89
98
  }
99
+ return true;
90
100
  }
91
101
 
92
102
  async function assertSecureFile(filePath: string): Promise<Awaited<ReturnType<typeof lstat>> | null> {
@@ -115,7 +125,9 @@ async function assertSecurePath(filePath: string, options: SecureStoreOptions):
115
125
  if (!isAbsolute(filePath) || resolve(filePath) !== filePath || dirname(filePath) !== secureRoot) {
116
126
  throw new Error(`Borg credential store file path ${filePath} is not canonical`);
117
127
  }
118
- await assertSecureRoot(secureRoot, options.rootMode);
128
+ if (!await assertSecureRoot(secureRoot, options.rootMode, options.createRoot !== false)) {
129
+ throw Object.assign(new Error('Borg private store directory disappeared'), { code: 'ENOENT' });
130
+ }
119
131
  }
120
132
 
121
133
  /**
@@ -223,13 +235,28 @@ export async function atomicWrite0600(
223
235
  else await ensureParentDir(filePath);
224
236
  if (options.secureRoot) await assertSecureFile(filePath);
225
237
  const tmp = `${filePath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
226
- const handle = await open(tmp, 'wx', 0o600);
238
+ const handle = await open(tmp, options.verifyLeafIdentity
239
+ ? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW
240
+ : 'wx', 0o600);
241
+ let opened: Awaited<ReturnType<typeof handle.stat>> | null = null;
242
+ const cleanup = async () => {
243
+ if (options.verifyLeafIdentity) {
244
+ const current = await lstat(tmp).catch(() => null);
245
+ if (!opened || !current || current.dev !== opened.dev || current.ino !== opened.ino) return;
246
+ }
247
+ await unlink(tmp).catch(() => {});
248
+ };
227
249
  try {
250
+ opened = options.verifyLeafIdentity ? await handle.stat() : null;
251
+ if (opened && (!opened.isFile() || (opened.mode & 0o777) !== 0o600 ||
252
+ (expectedUid() !== null && opened.uid !== expectedUid()))) {
253
+ throw new Error('Borg private store temporary file is not private');
254
+ }
228
255
  await handle.writeFile(data);
229
256
  await handle.sync();
230
257
  } catch (err) {
231
258
  await handle.close().catch(() => {});
232
- await unlink(tmp).catch(() => {});
259
+ await cleanup();
233
260
  throw err;
234
261
  }
235
262
  await handle.close();
@@ -238,7 +265,17 @@ export async function atomicWrite0600(
238
265
  await assertSecurePath(filePath, options);
239
266
  await assertSecureFile(filePath);
240
267
  }
241
- await rename(tmp, filePath);
268
+ if (opened) {
269
+ const current = await lstat(tmp);
270
+ if (!current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino ||
271
+ current.uid !== opened.uid || (current.mode & 0o777) !== 0o600) {
272
+ throw new Error('Borg private store temporary file identity changed');
273
+ }
274
+ }
275
+ if (options.exclusive) {
276
+ await link(tmp, filePath);
277
+ await unlink(tmp);
278
+ } else await rename(tmp, filePath);
242
279
  const parent = await open(dirname(filePath), 'r');
243
280
  try {
244
281
  await parent.sync();
@@ -246,7 +283,7 @@ export async function atomicWrite0600(
246
283
  await parent.close();
247
284
  }
248
285
  } catch (err) {
249
- await unlink(tmp).catch(() => {});
286
+ await cleanup();
250
287
  throw err;
251
288
  }
252
289
  }
@@ -283,6 +320,20 @@ async function assertSecureStorePerms(
283
320
  }
284
321
  }
285
322
 
323
+ /** Identity drift is retryable only when inspecting an acquisition lock. */
324
+ export class StoreFileIdentityChangedError extends Error {
325
+ readonly code = 'STORE_FILE_IDENTITY_CHANGED';
326
+
327
+ constructor() {
328
+ super('Borg credential store file changed while it was being opened');
329
+ }
330
+ }
331
+
332
+ function isLockTurnover(error: unknown): boolean {
333
+ const code = (error as NodeJS.ErrnoException).code;
334
+ return code === 'ENOENT' || code === 'STORE_FILE_IDENTITY_CHANGED';
335
+ }
336
+
286
337
  /**
287
338
  * Read the store file, or null when it does not exist (ONLY the missing-file
288
339
  * no-op path initializes empty). When the file exists, the 0600-store + 0700-parent
@@ -310,7 +361,7 @@ export async function readStoreFile(
310
361
  try {
311
362
  const opened = await handle.stat();
312
363
  if (opened.dev !== before.dev || opened.ino !== before.ino) {
313
- throw new Error('Borg credential store file changed while it was being opened');
364
+ throw new StoreFileIdentityChangedError();
314
365
  }
315
366
  const raw = await handle.readFile('utf8');
316
367
  const after = await handle.stat();
@@ -399,7 +450,7 @@ export async function withStoreLock<T>(
399
450
  if (stored === null) continue;
400
451
  raw = stored;
401
452
  } catch (readErr) {
402
- if ((readErr as NodeJS.ErrnoException).code === 'ENOENT') continue; // released — retry
453
+ if (isLockTurnover(readErr)) continue; // released or replaced — retry
403
454
  throw readErr;
404
455
  }
405
456
  const held = parseLockPayload(raw);
@@ -422,7 +473,7 @@ export async function withStoreLock<T>(
422
473
  : {},
423
474
  );
424
475
  } catch (readErr) {
425
- if ((readErr as NodeJS.ErrnoException).code === 'ENOENT') continue;
476
+ if (isLockTurnover(readErr)) continue;
426
477
  throw readErr;
427
478
  }
428
479
  if (currentRaw === null || currentRaw !== raw) continue;
@@ -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 [certificate, configText] = await Promise.all([
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`;
@@ -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,8 +139,37 @@ 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);
134
- if (!inspected) return { state: 'unowned', lockPath };
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 {
135
173
  const { raw, stat: lockStat } = inspected;
136
174
  if (raw === null) {
137
175
  const now = (deps.now ?? (() => new Date()))();
@@ -200,6 +238,10 @@ async function tryCreateLease(
200
238
  lockPath: string,
201
239
  deps: StreamOwnerDeps
202
240
  ): Promise<StreamLease | null> {
241
+ if (deps.privateRoot) {
242
+ await privateLeaseDirectory(lockPath, deps, false);
243
+ await privateLeaseDirectory(takeoverPath(lockPath), deps, false);
244
+ }
203
245
  if (await pathExists(takeoverPath(lockPath))) return null;
204
246
  try {
205
247
  await fs.mkdir(lockPath, { mode: 0o700 });
@@ -210,22 +252,22 @@ async function tryCreateLease(
210
252
 
211
253
  const record = makeRecord(deps);
212
254
  try {
213
- await (deps.writeRecord ?? writeRecord)(lockPath, record);
255
+ await writeLeaseRecord(lockPath, record, deps);
214
256
  if (await pathExists(takeoverPath(lockPath))) {
215
- await cleanupFailedInitialization(lockPath, record).catch(() => {});
257
+ await cleanupFailedInitialization(lockPath, record, deps).catch(() => {});
216
258
  return null;
217
259
  }
218
260
  } catch (err) {
219
- await cleanupFailedInitialization(lockPath, record).catch(() => {});
261
+ await cleanupFailedInitialization(lockPath, record, deps).catch(() => {});
220
262
  throw err;
221
263
  }
222
- const created = await readBoundOwner(lockPath);
264
+ const created = await readBoundOwner(lockPath, deps);
223
265
  const createdRecord = created?.raw ? parseOwnershipRecord(created.raw) : null;
224
266
  if (
225
267
  !created || !createdRecord ||
226
268
  !sameOwner(record, createdRecord)
227
269
  ) {
228
- await cleanupFailedInitialization(lockPath, record).catch(() => {});
270
+ await cleanupFailedInitialization(lockPath, record, deps).catch(() => {});
229
271
  throw new Error('Borg stream lease initialization lost ownership');
230
272
  }
231
273
  return makeLease(
@@ -243,7 +285,8 @@ async function tryCreateLease(
243
285
  */
244
286
  async function cleanupFailedInitialization(
245
287
  lockPath: string,
246
- attempted: StreamOwnerRecord
288
+ attempted: StreamOwnerRecord,
289
+ deps: StreamOwnerDeps,
247
290
  ): Promise<void> {
248
291
  const cleanupPath = `${lockPath}.failed-${attempted.processNonce}-${Date.now()}`;
249
292
  try {
@@ -252,7 +295,7 @@ async function cleanupFailedInitialization(
252
295
  if (err?.code === 'ENOENT') return;
253
296
  throw err;
254
297
  }
255
- const current = await readOwnershipRecord(cleanupPath);
298
+ const current = await readOwnershipRecord(cleanupPath, deps);
256
299
  if (current && (current.pid !== attempted.pid || current.processNonce !== attempted.processNonce)) {
257
300
  try {
258
301
  await fs.rename(cleanupPath, lockPath);
@@ -274,6 +317,10 @@ async function moveStaleLockAside(
274
317
  deps: StreamOwnerDeps
275
318
  ): Promise<boolean> {
276
319
  const claimPath = takeoverPath(lockPath);
320
+ if (deps.privateRoot) {
321
+ await privateLeaseDirectory(lockPath, deps, false);
322
+ await privateLeaseDirectory(claimPath, deps, false);
323
+ }
277
324
  try {
278
325
  await fs.rename(lockPath, claimPath);
279
326
  } catch (err: any) {
@@ -301,13 +348,14 @@ async function moveStaleLockAside(
301
348
  processNonce: deps.processNonce ?? processNonce,
302
349
  claimedAt: (deps.now ?? (() => new Date()))().toISOString(),
303
350
  };
304
- await fs.writeFile(
351
+ if (deps.privateRoot) await writePrivateLeaseFile(claimPath, TAKEOVER_FILE, claimant, deps, true);
352
+ else await fs.writeFile(
305
353
  path.join(claimPath, TAKEOVER_FILE),
306
354
  JSON.stringify(claimant, null, 2) + '\n',
307
355
  { flag: 'wx', mode: 0o600 },
308
356
  );
309
357
  await deps.beforeTakeoverVerify?.(claimPath);
310
- const verified = await readOwnershipRecord(claimPath);
358
+ const verified = await readOwnershipRecord(claimPath, deps);
311
359
  const verifiedStat = await fs.stat(claimPath).catch(() => null);
312
360
  if (!isStillReclaimable(snapshot, verified, verifiedStat, staleMs, deps)) {
313
361
  await fs.unlink(path.join(claimPath, TAKEOVER_FILE)).catch(() => {});
@@ -344,6 +392,10 @@ async function recoverTakeoverClaim(
344
392
  deps: StreamOwnerDeps,
345
393
  ): Promise<boolean> {
346
394
  const claimPath = takeoverPath(lockPath);
395
+ if (deps.privateRoot) {
396
+ await privateLeaseDirectory(lockPath, deps, false);
397
+ await privateLeaseDirectory(claimPath, deps, false);
398
+ }
347
399
  let claimStat;
348
400
  try {
349
401
  claimStat = await fs.stat(claimPath);
@@ -353,9 +405,14 @@ async function recoverTakeoverClaim(
353
405
  }
354
406
 
355
407
  const now = (deps.now ?? (() => new Date()))();
408
+ const privateClaim = deps.privateRoot
409
+ ? await readStoreFile(path.join(claimPath, TAKEOVER_FILE), { secureRoot: claimPath, createRoot: false })
410
+ : undefined;
356
411
  let claimant: { pid: number; processNonce: string; claimedAt: string } | null = null;
357
412
  try {
358
- const parsed = JSON.parse(await fs.readFile(path.join(claimPath, TAKEOVER_FILE), 'utf8'));
413
+ const parsed = JSON.parse((deps.privateRoot
414
+ ? privateClaim
415
+ : await fs.readFile(path.join(claimPath, TAKEOVER_FILE), 'utf8')) ?? 'null');
359
416
  if (
360
417
  parsed?.schemaVersion === 1 &&
361
418
  Number.isSafeInteger(parsed.pid) && parsed.pid > 0 &&
@@ -377,7 +434,7 @@ async function recoverTakeoverClaim(
377
434
  ) return false;
378
435
  if (!claimant && claimAge <= STREAM_OWNER_TAKEOVER_STALE_MS) return false;
379
436
 
380
- const owner = await readOwnershipRecord(claimPath);
437
+ const owner = await readOwnershipRecord(claimPath, deps);
381
438
  const ownerHeartbeat = owner ? Date.parse(owner.heartbeatAt) : Number.NaN;
382
439
  const ownerAge = Number.isFinite(ownerHeartbeat)
383
440
  ? now.getTime() - ownerHeartbeat
@@ -481,7 +538,7 @@ function makeLease(
481
538
  ownedRecord,
482
539
  deps,
483
540
  async (claimPath) => {
484
- await (deps.writeRecord ?? writeRecord)(claimPath, next);
541
+ await writeLeaseRecord(claimPath, next, deps);
485
542
  return 'restore';
486
543
  },
487
544
  );
@@ -511,6 +568,10 @@ async function mutateClaimedLease(
511
568
  mutation: (claimPath: string) => Promise<'restore' | 'remove'>,
512
569
  ): Promise<boolean> {
513
570
  const claimPath = takeoverPath(lockPath);
571
+ if (deps.privateRoot) {
572
+ await privateLeaseDirectory(lockPath, deps, false);
573
+ await privateLeaseDirectory(claimPath, deps, false);
574
+ }
514
575
  try {
515
576
  await fs.rename(lockPath, claimPath);
516
577
  } catch (error: any) {
@@ -532,13 +593,14 @@ async function mutateClaimedLease(
532
593
  processNonce: deps.processNonce ?? processNonce,
533
594
  claimedAt: (deps.now ?? (() => new Date()))().toISOString(),
534
595
  };
535
- await fs.writeFile(
596
+ if (deps.privateRoot) await writePrivateLeaseFile(claimPath, TAKEOVER_FILE, claimant, deps, true);
597
+ else await fs.writeFile(
536
598
  path.join(claimPath, TAKEOVER_FILE),
537
599
  JSON.stringify(claimant, null, 2) + '\n',
538
600
  { flag: 'wx', mode: 0o600 },
539
601
  );
540
602
 
541
- const inspected = await readBoundOwner(claimPath);
603
+ const inspected = await readBoundOwner(claimPath, deps);
542
604
  const current = inspected?.raw ? parseOwnershipRecord(inspected.raw) : null;
543
605
  if (
544
606
  !inspected || !sameIdentity(identity, inspected.stat) || !current ||
@@ -590,7 +652,11 @@ function sameOwner(left: StreamOwnerRecord, right: StreamOwnerRecord): boolean {
590
652
  left.cubeName === right.cubeName;
591
653
  }
592
654
 
593
- async function readOwnershipRecord(lockPath: string): Promise<StreamOwnerRecord | null> {
655
+ async function readOwnershipRecord(lockPath: string, deps: StreamOwnerDeps = {}): Promise<StreamOwnerRecord | null> {
656
+ if (deps.privateRoot) {
657
+ const inspected = await readBoundOwner(lockPath, deps);
658
+ return inspected?.raw ? parseOwnershipRecord(inspected.raw) : null;
659
+ }
594
660
  try {
595
661
  const raw = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
596
662
  return parseOwnershipRecord(raw);
@@ -608,10 +674,11 @@ function parseOwnershipRecord(raw: string): StreamOwnerRecord | null {
608
674
  }
609
675
  }
610
676
 
611
- async function readBoundOwner(lockPath: string): Promise<{
677
+ async function readBoundOwner(lockPath: string, deps: StreamOwnerDeps = {}): Promise<{
612
678
  raw: string | null;
613
679
  stat: { dev: number; ino: number; mtimeMs: number };
614
680
  } | null> {
681
+ if (deps.privateRoot && !await privateLeaseDirectory(lockPath, deps, false)) return null;
615
682
  for (let attempt = 0; attempt < 3; attempt += 1) {
616
683
  let handle;
617
684
  try {
@@ -624,8 +691,13 @@ async function readBoundOwner(lockPath: string): Promise<{
624
691
  const openedStat = await handle.stat();
625
692
  let raw: string | null;
626
693
  try {
627
- raw = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
694
+ raw = deps.privateRoot
695
+ ? await readStoreFile(path.join(lockPath, OWNER_FILE), { secureRoot: lockPath, createRoot: false })
696
+ : await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
628
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;
629
701
  if (error?.code !== 'ENOENT') throw error;
630
702
  raw = null;
631
703
  }
@@ -652,6 +724,43 @@ function sameIdentity(
652
724
  return left.dev === right.dev && left.ino === right.ino;
653
725
  }
654
726
 
727
+ /** Apply the existing private-store directory policy without creating on reads. */
728
+ async function privateLeaseDirectory(directory: string, deps: StreamOwnerDeps, create: boolean): Promise<boolean> {
729
+ const { root, boundary } = deps.privateRoot!;
730
+ const ancestors = path.relative(boundary, root);
731
+ if (ancestors.startsWith('..') || path.isAbsolute(ancestors)) {
732
+ throw new Error('Borg private lease root escapes its trusted boundary');
733
+ }
734
+ const relative = path.relative(root, directory);
735
+ if (path.resolve(directory) !== directory || relative.startsWith('..') || path.isAbsolute(relative)) {
736
+ throw new Error('Borg private lease path escapes its private root');
737
+ }
738
+ let ancestor = boundary;
739
+ for (const component of ancestors.split(path.sep).filter(Boolean)) {
740
+ if (!await assertSecureRoot(ancestor, 'owner-controlled', create)) return false;
741
+ ancestor = path.join(ancestor, component);
742
+ }
743
+ if (!await assertSecureRoot(root, 'private', create)) return false;
744
+ let current = root;
745
+ for (const component of relative.split(path.sep).filter(Boolean)) {
746
+ current = path.join(current, component);
747
+ if (!await assertSecureRoot(current, 'private', create)) return false;
748
+ }
749
+ return true;
750
+ }
751
+
752
+ async function writePrivateLeaseFile(directory: string, name: string, value: unknown, deps: StreamOwnerDeps, exclusive = false): Promise<void> {
753
+ if (!await privateLeaseDirectory(directory, deps, false)) throw new Error('Borg private lease directory disappeared');
754
+ await atomicWrite0600(path.join(directory, name), JSON.stringify(value, null, 2) + '\n', {
755
+ secureRoot: directory, verifyLeafIdentity: true, exclusive, createRoot: false,
756
+ });
757
+ }
758
+
759
+ async function writeLeaseRecord(directory: string, record: StreamOwnerRecord, deps: StreamOwnerDeps): Promise<void> {
760
+ if (deps.privateRoot) await writePrivateLeaseFile(directory, OWNER_FILE, record, deps);
761
+ else await (deps.writeRecord ?? writeRecord)(directory, record);
762
+ }
763
+
655
764
  async function writeRecord(lockPath: string, record: StreamOwnerRecord): Promise<void> {
656
765
  const ownerPath = path.join(lockPath, OWNER_FILE);
657
766
  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