borgmcp 2.14.2 → 2.15.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 (97) hide show
  1. package/dist/agent-runtime.d.ts +5 -0
  2. package/dist/agent-runtime.d.ts.map +1 -1
  3. package/dist/agent-runtime.js +14 -0
  4. package/dist/agent-runtime.js.map +1 -1
  5. package/dist/assimilate-cmd.d.ts +3 -0
  6. package/dist/assimilate-cmd.d.ts.map +1 -1
  7. package/dist/assimilate-cmd.js +33 -7
  8. package/dist/assimilate-cmd.js.map +1 -1
  9. package/dist/assimilate-deps.d.ts.map +1 -1
  10. package/dist/assimilate-deps.js +17 -3
  11. package/dist/assimilate-deps.js.map +1 -1
  12. package/dist/claude.d.ts.map +1 -1
  13. package/dist/claude.js +2 -1
  14. package/dist/claude.js.map +1 -1
  15. package/dist/cleanup-cmd.d.ts.map +1 -1
  16. package/dist/cleanup-cmd.js +2 -2
  17. package/dist/cleanup-cmd.js.map +1 -1
  18. package/dist/cli-tool-approval.d.ts.map +1 -1
  19. package/dist/cli-tool-approval.js +3 -2
  20. package/dist/cli-tool-approval.js.map +1 -1
  21. package/dist/codex-remote.d.ts.map +1 -1
  22. package/dist/codex-remote.js +4 -3
  23. package/dist/codex-remote.js.map +1 -1
  24. package/dist/config-utils.d.ts +16 -1
  25. package/dist/config-utils.d.ts.map +1 -1
  26. package/dist/config-utils.js +176 -17
  27. package/dist/config-utils.js.map +1 -1
  28. package/dist/credential-paths.d.ts.map +1 -1
  29. package/dist/credential-paths.js +2 -3
  30. package/dist/credential-paths.js.map +1 -1
  31. package/dist/cubes.js +2 -2
  32. package/dist/cubes.js.map +1 -1
  33. package/dist/foreign-path-reminder.d.ts +12 -0
  34. package/dist/foreign-path-reminder.d.ts.map +1 -0
  35. package/dist/foreign-path-reminder.js +122 -0
  36. package/dist/foreign-path-reminder.js.map +1 -0
  37. package/dist/launch-access.d.ts +22 -0
  38. package/dist/launch-access.d.ts.map +1 -0
  39. package/dist/launch-access.js +27 -0
  40. package/dist/launch-access.js.map +1 -0
  41. package/dist/launch-all-deps.d.ts.map +1 -1
  42. package/dist/launch-all-deps.js +2 -2
  43. package/dist/launch-all-deps.js.map +1 -1
  44. package/dist/lifecycle-log-guard.js +2 -2
  45. package/dist/lifecycle-log-guard.js.map +1 -1
  46. package/dist/local-server-cursor.js +2 -2
  47. package/dist/local-server-cursor.js.map +1 -1
  48. package/dist/opencode-plugin.js +2 -2
  49. package/dist/opencode-plugin.js.map +1 -1
  50. package/dist/private-root.d.ts +17 -0
  51. package/dist/private-root.d.ts.map +1 -1
  52. package/dist/private-root.js +62 -2
  53. package/dist/private-root.js.map +1 -1
  54. package/dist/seat-store.d.ts.map +1 -1
  55. package/dist/seat-store.js +4 -3
  56. package/dist/seat-store.js.map +1 -1
  57. package/dist/seats.d.ts.map +1 -1
  58. package/dist/seats.js +2 -2
  59. package/dist/seats.js.map +1 -1
  60. package/dist/self-path.d.ts +2 -0
  61. package/dist/self-path.d.ts.map +1 -1
  62. package/dist/self-path.js +4 -0
  63. package/dist/self-path.js.map +1 -1
  64. package/dist/server-trust.d.ts.map +1 -1
  65. package/dist/server-trust.js +3 -3
  66. package/dist/server-trust.js.map +1 -1
  67. package/dist/setup-confirm.d.ts +1 -1
  68. package/dist/setup-confirm.js +1 -1
  69. package/dist/stream-owner.js +2 -2
  70. package/dist/stream-owner.js.map +1 -1
  71. package/docs/EXTRACTION_PROVENANCE.md +2 -2
  72. package/docs/LOCAL_SERVER.md +23 -0
  73. package/docs/RELEASING.md +123 -1
  74. package/package.json +2 -1
  75. package/src/agent-runtime.ts +17 -0
  76. package/src/assimilate-cmd.ts +49 -7
  77. package/src/assimilate-deps.ts +21 -3
  78. package/src/claude.ts +2 -0
  79. package/src/cleanup-cmd.ts +2 -2
  80. package/src/cli-tool-approval.ts +3 -2
  81. package/src/codex-remote.ts +4 -2
  82. package/src/config-utils.ts +186 -18
  83. package/src/credential-paths.ts +2 -3
  84. package/src/cubes.ts +2 -2
  85. package/src/foreign-path-reminder.ts +141 -0
  86. package/src/launch-access.ts +40 -0
  87. package/src/launch-all-deps.ts +2 -2
  88. package/src/lifecycle-log-guard.ts +2 -2
  89. package/src/local-server-cursor.ts +2 -2
  90. package/src/opencode-plugin.ts +2 -2
  91. package/src/private-root.ts +62 -2
  92. package/src/seat-store.ts +4 -3
  93. package/src/seats.ts +2 -2
  94. package/src/self-path.ts +5 -0
  95. package/src/server-trust.ts +3 -3
  96. package/src/setup-confirm.ts +1 -1
  97. package/src/stream-owner.ts +2 -2
package/docs/RELEASING.md CHANGED
@@ -216,7 +216,7 @@ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.14.
216
216
  successfully published that exact source as `borgmcp@2.14.1`; the same-run artifact report records integrity
217
217
  `sha512-VqkOp3jo/KnzgusCk98TFj/U8U7Q/gt8agvIbjiSWrV0lIHnsD+A+7w3C+GMQeyA08KXxRuZDF3No9sDomqMTQ==`.
218
218
  Never move, replace, reuse, or rerun that tag or workflow. The next candidate
219
- uses the unused `v2.14.2` identity from a fresh reviewed protected-main commit
219
+ uses the unused `v2.15.0` identity from a fresh reviewed protected-main commit
220
220
  and requires the complete release gate again.
221
221
 
222
222
  ## Release Prerequisites
@@ -340,6 +340,128 @@ Before preparing a candidate, independently verify:
340
340
 
341
341
  ## Release Workflow
342
342
 
343
+ ### Clean-environment rig lifecycle
344
+
345
+ Every clean-environment verification rig has an explicit identity and an
346
+ explicit end-of-life. Keep filesystem workspaces under the seat's disposable
347
+ `~/.borg/scratch/<seat>/` root, and give each workspace a name beginning with
348
+ `borg-rig-` followed by the seat, purpose, and a unique suffix. Before the
349
+ first command that can create files, anchor the rig as its own npm project:
350
+
351
+ ```sh
352
+ BORG_SEAT_LABEL="${BORG_SEAT_LABEL:?set the seat label}"
353
+ BORG_SCRATCH_ROOT="${BORG_SCRATCH_ROOT:-$HOME/.borg/scratch/$BORG_SEAT_LABEL}"
354
+ RIG_NONCE="${RIG_NONCE:-$(date +%Y%m%d%H%M%S)-$$}"
355
+ RIG_ID="borg-rig-${BORG_SEAT_LABEL}-release-${RIG_NONCE}"
356
+ RIG_ROOT="$BORG_SCRATCH_ROOT/$RIG_ID"
357
+ mkdir -p "$RIG_ROOT"
358
+ printf '%s\n' '{"private":true}' > "$RIG_ROOT/package.json"
359
+ ```
360
+
361
+ The manifest anchor is required before any `npm install`, `npm update`, or
362
+ other npm command. Without it, npm can walk up from an empty scratch directory
363
+ and write the operator's `package.json` instead of creating project-local
364
+ state. The committed `release:exercise` follows this rule for its temporary
365
+ consumer; other QA scripts and manual rigs must do the same.
366
+
367
+ The system temporary root is shared with the whole machine, so an unbounded
368
+ listing there is not an inspectable cleanup check. Bound that leg by the
369
+ invoking user and the run-window markers, which select directories whose
370
+ timestamps fall within this run window without requiring a prefix inventory.
371
+ New `mkdtemp` prefixes therefore require no documentation change. Keep both
372
+ time predicates and the ownership predicate: removing the run-window bound
373
+ turns a populated shared temp root back into an uninspectable listing.
374
+
375
+ On a machine where several runs share one user account, this listing can
376
+ include another run's live workspace. Removal is scoped to paths created by
377
+ this run; anything else in the output is reported, not deleted. Unrelated
378
+ same-user temporary directories created during the window can therefore
379
+ appear in the result, so never remove a path merely because this listing found
380
+ it.
381
+
382
+ ```sh
383
+ TEMP_ROOT="${TMPDIR:-/tmp}"
384
+ TEMP_OWNER="$(id -un)"
385
+ TEMP_SCAN_START="$TEMP_ROOT/.${RIG_ID}-temp-scan-start"
386
+ TEMP_SCAN_END="$TEMP_ROOT/.${RIG_ID}-temp-scan-end"
387
+
388
+ list_recent_owned_temp_rigs() {
389
+ find "$TEMP_ROOT" -mindepth 1 -prune \
390
+ -type d \
391
+ -user "$TEMP_OWNER" \
392
+ -newer "$TEMP_SCAN_START" \
393
+ ! -newer "$TEMP_SCAN_END" \
394
+ -print
395
+ }
396
+
397
+ touch "$TEMP_SCAN_START"
398
+ ```
399
+
400
+ Container-backed rigs use the same `RIG_ID` as the container name and carry
401
+ both labels below. `--rm` is preferred; a runtime without automatic removal
402
+ must remove the exact `RIG_ID` in its cleanup path and retain the labels for a
403
+ bounded sweep:
404
+
405
+ ```sh
406
+ RIG_IMAGE="${RIG_IMAGE:?set the rig image}"
407
+ docker run --rm \
408
+ --name "$RIG_ID" \
409
+ --label borg-rig=1 \
410
+ --label "borg-rig-owner=$BORG_SEAT_LABEL" \
411
+ "$RIG_IMAGE"
412
+ ```
413
+
414
+ Register the exact-target cleanup before launching the rig:
415
+
416
+ ```sh
417
+ cleanup_done=0
418
+ cleanup() {
419
+ [ "$cleanup_done" -eq 0 ] || return
420
+ cleanup_done=1
421
+ trap - EXIT HUP INT TERM
422
+ docker container rm --force "$RIG_ID" >/dev/null 2>&1 || true
423
+ rm -rf -- "$RIG_ROOT"
424
+ if [ -e "$TEMP_SCAN_START" ]; then
425
+ touch "$TEMP_SCAN_END"
426
+ list_recent_owned_temp_rigs
427
+ fi
428
+ rm -f -- "$TEMP_SCAN_START" "$TEMP_SCAN_END"
429
+ }
430
+ trap cleanup EXIT
431
+ trap 'exit 129' HUP
432
+ trap 'exit 130' INT
433
+ trap 'exit 143' TERM
434
+ ```
435
+
436
+ List before starting and after cleanup. The filters distinguish this seat's
437
+ rigs from unrelated containers on a shared host:
438
+
439
+ ```sh
440
+ docker container ls --all \
441
+ --filter label=borg-rig=1 \
442
+ --filter "label=borg-rig-owner=${BORG_SEAT_LABEL:?}" \
443
+ --format '{{.ID}}\t{{.Names}}\t{{.Status}}'
444
+ find "$BORG_SCRATCH_ROOT" -mindepth 1 -print
445
+ find "$BORG_SCRATCH_ROOT" -name 'borg-rig-*' -print
446
+ ```
447
+
448
+ The unfiltered filesystem listings expose legacy names as well as conforming
449
+ rigs. The bounded temporary-directory function covers tools that use `mkdtemp`
450
+ instead of the seat scratch root. Run the container and scratch listings before
451
+ launch and again from the cleanup path; the temporary-directory function runs
452
+ after cleanup, between the start and end markers. Inspect its results and do
453
+ not remove unrelated temporary files.
454
+
455
+ Register cleanup before launching any process, run it on success and failure,
456
+ and do not deliver a verdict until the container listing, both scratch listings,
457
+ and the bounded temporary-directory listing show no rig owned by the seat from
458
+ this run. For a non-`--rm` container runtime,
459
+ remove only the named rig or the same owner label; never prune unrelated
460
+ containers. A completed verification
461
+ therefore implies zero running or stopped rig containers and no rig workspace
462
+ created by this run left in either the seat scratch root or the system temporary
463
+ directory.
464
+
343
465
  ### Pre-tag composed exercise
344
466
 
345
467
  Before creating a release tag, exercise the packed client against the selected
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "2.14.2",
3
+ "version": "2.15.0",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,6 +19,7 @@
19
19
  "borg-regen": "./dist/regen.js",
20
20
  "borg-clear-rewake": "./dist/clear-rewake.js",
21
21
  "borg-log-audit": "./dist/log-audit.js",
22
+ "borg-foreign-path-reminder": "./dist/foreign-path-reminder.js",
22
23
  "borg-inbox-monitor": "./dist/inbox-monitor.js"
23
24
  },
24
25
  "scripts": {
@@ -5,6 +5,8 @@
5
5
  * agent_kind without re-assimilating.
6
6
  */
7
7
 
8
+ import { BORG_STATE_ROOT_ENV } from './private-root.js';
9
+
8
10
  export type AgentKind = 'claude' | 'codex' | 'opencode';
9
11
 
10
12
  /** Pinned into MCP-child environments by Borg launch paths. */
@@ -74,3 +76,18 @@ export function codexRemoteWakeConfigArgs(enabled: boolean = true): string[] {
74
76
  `mcp_servers.borg.env.${BORG_CODEX_REMOTE_WAKE_ENV}="${enabled ? '1' : '0'}"`,
75
77
  ];
76
78
  }
79
+
80
+ /**
81
+ * Codex MCP children do not inherit the wrapper environment. Pin the explicit
82
+ * Borg state root into the per-launch child environment when one is active.
83
+ */
84
+ export function codexStateRootConfigArgs(
85
+ env: NodeJS.ProcessEnv = process.env
86
+ ): string[] {
87
+ const stateRoot = env[BORG_STATE_ROOT_ENV];
88
+ if (stateRoot === undefined) return [];
89
+ return [
90
+ '-c',
91
+ `mcp_servers.borg.env.${BORG_STATE_ROOT_ENV}=${JSON.stringify(stateRoot)}`,
92
+ ];
93
+ }
@@ -23,6 +23,7 @@ import { codexBorgSessionConfigArgs } from './launch-gate.js';
23
23
  import {
24
24
  codexAgentKindConfigArgs,
25
25
  codexRemoteWakeConfigArgs,
26
+ codexStateRootConfigArgs,
26
27
  withAgentRuntimeEnv,
27
28
  } from './agent-runtime.js';
28
29
  import type { BorgCli } from './cubes.js';
@@ -74,6 +75,14 @@ import type { SeatBinding, BindPendingSeatOutcome } from './seats.js';
74
75
  import { createHash } from 'node:crypto';
75
76
  import { buildOpenCodeLaunchArgs, type LaunchApprovalDecision } from './cli-tool-approval.js';
76
77
  import { resolveWorkingRepo, type WorkingRepo } from './working-repo.js';
78
+ import {
79
+ BORG_LAUNCH_CLI_ENV,
80
+ BORG_LAUNCH_SCRATCH_ENV,
81
+ BORG_LAUNCH_WORKTREE_ENV,
82
+ codexLaunchDirectoryArgs,
83
+ scratchRootForSeat,
84
+ type LaunchAccessPaths,
85
+ } from './launch-access.js';
77
86
  import type {
78
87
  AssociateRepositoryCubeResponse,
79
88
  CreateCubeRepository,
@@ -287,6 +296,12 @@ export interface AssimilateDeps {
287
296
  // launch root's .claude/settings.local.json (project-local; idempotent).
288
297
  // Real wiring = config-utils addProjectSessionStartHook.
289
298
  installProjectSessionHook: (projectRoot: string) => void;
299
+ /** Pre-authorize only the launched seat's worktree + scratch paths. */
300
+ provisionLaunchAccess?: (
301
+ cli: BorgCli,
302
+ projectRoot: string,
303
+ paths: LaunchAccessPaths,
304
+ ) => void;
290
305
 
291
306
  /** gh#27: optional test seam — when set, selectAssimilationAuthority uses
292
307
  * this instead of prompting/failing. Not wired in production. */
@@ -1807,6 +1822,22 @@ export async function runAssimilate(
1807
1822
  spawnedWorktreePath = deps.cwd();
1808
1823
  }
1809
1824
 
1825
+ // ----- Step 7b: provision launch access before persisting/launching -----
1826
+ // The launched process gets exactly its current worktree plus a stable,
1827
+ // disposable per-seat scratch root. Provision this before FINALIZE so a
1828
+ // failed config write cannot produce a saved seat that launches without
1829
+ // its promised path grants.
1830
+ const agentCwd = deps.cwd(); // post-chdir if step 3 spawned a worktree
1831
+ const seatWorktree = deps.findProjectRoot(agentCwd);
1832
+ const monitorStateRoot = monitorStateRootForWorktree(seatWorktree);
1833
+ const scratchRoot = scratchRootForSeat(deps.homedir(), result.drone_label, result.drone_id);
1834
+ const launchAccessPaths: LaunchAccessPaths = {
1835
+ // Access is granted at the repository root even when the harness starts in
1836
+ // a nested package. The launch cwd remains the operator's chosen subdir.
1837
+ worktree: seatWorktree,
1838
+ scratch: scratchRoot,
1839
+ };
1840
+
1810
1841
  // ----- Step 8: persist the binding (narrow rollback — worktree exists if spawned) -----
1811
1842
  const activeCube: ActiveCube = {
1812
1843
  cubeId: result.cube_id,
@@ -1836,6 +1867,19 @@ export async function runAssimilate(
1836
1867
  }
1837
1868
  };
1838
1869
 
1870
+ try {
1871
+ deps.mkdirp(scratchRoot);
1872
+ deps.provisionLaunchAccess?.(cli, seatWorktree, launchAccessPaths);
1873
+ } catch (err) {
1874
+ const message = err instanceof Error ? err.message : String(err);
1875
+ deps.stderr(
1876
+ `Borg could not pre-authorize the ${cli} launch paths: ${message}. ` +
1877
+ 'No agent was launched; correct the local configuration and retry.\n',
1878
+ );
1879
+ rollbackWorktree();
1880
+ return 1;
1881
+ }
1882
+
1839
1883
  // Local-server authority: drive the COMPOSITE cube-owned FINALIZE (Race 2).
1840
1884
  // The cube lock is held OUTER across revalidate → binding-write → activate; the
1841
1885
  // typed expectation is declared HERE at the orchestration layer (reattach =
@@ -1945,13 +1989,6 @@ export async function runAssimilate(
1945
1989
  }
1946
1990
  }
1947
1991
 
1948
- // The worktree, not a reminted drone UUID, is the stable local seat identity
1949
- // for monitor runtime state. Capture it once before the lazy GC and launch
1950
- // paths so both use the exact same explicit root.
1951
- const agentCwd = deps.cwd(); // post-chdir if step 3 spawned a worktree
1952
- const seatWorktree = deps.findProjectRoot(agentCwd);
1953
- const monitorStateRoot = monitorStateRootForWorktree(seatWorktree);
1954
-
1955
1992
  // gh#793: best-effort GC of orphaned inbox files (evicted/dead drones) in the
1956
1993
  // cube just joined — lazy-on-assimilate, no cron/new command. NEVER blocks or
1957
1994
  // fails the assimilate (whole call swallowed). Local-only signal (CubeDetail
@@ -2082,6 +2119,9 @@ export async function runAssimilate(
2082
2119
  ...(withAgentRuntimeEnv(process.env, cli) as Record<string, string>),
2083
2120
  ...modelEnv.set,
2084
2121
  BORG_SESSION: '1',
2122
+ [BORG_LAUNCH_CLI_ENV]: cli,
2123
+ [BORG_LAUNCH_WORKTREE_ENV]: seatWorktree,
2124
+ [BORG_LAUNCH_SCRATCH_ENV]: scratchRoot,
2085
2125
  };
2086
2126
  if (cli === 'opencode' && launchApproval.openCodePermission) {
2087
2127
  childEnv.OPENCODE_PERMISSION = launchApproval.openCodePermission;
@@ -2127,10 +2167,12 @@ export async function runAssimilate(
2127
2167
  // off when no socket is available, overriding legacy static configs that
2128
2168
  // formerly used this transport marker as Codex identity.
2129
2169
  launchArgs = [
2170
+ ...codexLaunchDirectoryArgs(launchAccessPaths),
2130
2171
  ...launchApproval.codexArgs,
2131
2172
  ...codexBorgSessionConfigArgs(),
2132
2173
  ...codexAgentKindConfigArgs(),
2133
2174
  ...codexRemoteWakeConfigArgs(codexSocketPath !== null),
2175
+ ...codexStateRootConfigArgs(),
2134
2176
  ...remoteArgs,
2135
2177
  ...withCodexCwdArg(launchArgs, agentCwd),
2136
2178
  ];
@@ -11,7 +11,7 @@
11
11
  import { spawnSync, spawn as spawnChild } from 'node:child_process';
12
12
  import { randomBytes } from 'node:crypto';
13
13
  import { existsSync, mkdirSync } from 'node:fs';
14
- import { hostname as osHostname, homedir as osHomedir } from 'node:os';
14
+ import { hostname as osHostname } from 'node:os';
15
15
  import { createInterface } from 'node:readline/promises';
16
16
  import prompts from 'prompts';
17
17
  import { readinessProbeEnv } from './readiness-probe.js';
@@ -62,7 +62,13 @@ import {
62
62
  inboxPathForDrone,
63
63
  setCodexWakeTarget,
64
64
  } from './cubes.js';
65
- import { addProjectSessionStartHook } from './config-utils.js';
65
+ import {
66
+ addClaudeLaunchAccess,
67
+ addCodexForeignPathReminderHook,
68
+ addOpenCodeLaunchAccess,
69
+ addProjectSessionStartHook,
70
+ } from './config-utils.js';
71
+ import type { LaunchAccessPaths } from './launch-access.js';
66
72
  import { findPendingServerEnrollment } from './config.js';
67
73
  import { setTerminalTitle as setTitle } from './terminal-title.js';
68
74
  import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
@@ -77,6 +83,7 @@ import {
77
83
  saveRepositoryAssociation,
78
84
  } from './repository-identity.js';
79
85
  import { PromptInterruptedError } from './repository-cube-init.js';
86
+ import { borgHomeRoot } from './private-root.js';
80
87
 
81
88
  /**
82
89
  * Wraps the readline question operation with the production interruption
@@ -126,7 +133,7 @@ export function buildDefaultAssimilateDeps(
126
133
  // gh#556 Part 1: ~/.borg/worktrees relocation. homedir seams $HOME;
127
134
  // mkdirp is a plain recursive create (NO chmod of existing parents — the
128
135
  // ~/.borg credentials file's perms stay untouched).
129
- homedir: () => osHomedir(),
136
+ homedir: () => borgHomeRoot(),
130
137
  mkdirp: (dir) => mkdirSync(dir, { recursive: true }),
131
138
  preparePrivateRoot: () => ensurePrivateBorgConfigRoot(),
132
139
  exec: (cmd, args, cwd, env) =>
@@ -235,6 +242,17 @@ export function buildDefaultAssimilateDeps(
235
242
  installProjectSessionHook: (projectRoot) => {
236
243
  addProjectSessionStartHook(projectRoot);
237
244
  },
245
+ provisionLaunchAccess: (cli, projectRoot, paths: LaunchAccessPaths) => {
246
+ if (cli === 'claude') {
247
+ addClaudeLaunchAccess(projectRoot, paths);
248
+ } else if (cli === 'codex') {
249
+ // Codex receives the path grant on the launch command itself. Its
250
+ // native PreToolUse hook is global but reads this launch's scoped env.
251
+ addCodexForeignPathReminderHook();
252
+ } else {
253
+ addOpenCodeLaunchAccess(projectRoot, paths);
254
+ }
255
+ },
238
256
 
239
257
  // #1015: discovery is advisory but still verifies the server-owned CA.
240
258
  detectLocalServer: async () =>
package/src/claude.ts CHANGED
@@ -62,6 +62,7 @@ import {
62
62
  BORG_CODEX_REMOTE_WAKE_ENV,
63
63
  codexAgentKindConfigArgs,
64
64
  codexRemoteWakeConfigArgs,
65
+ codexStateRootConfigArgs,
65
66
  withAgentRuntimeEnv,
66
67
  } from './agent-runtime.js';
67
68
  import { findLoadedCodexThread } from './codex-app-server.js';
@@ -531,6 +532,7 @@ async function main() {
531
532
  ...codexBorgSessionConfigArgs(),
532
533
  ...codexAgentKindConfigArgs(),
533
534
  ...codexRemoteWakeConfigArgs(codexSocketPath !== null),
535
+ ...codexStateRootConfigArgs(),
534
536
  ...remoteArgs,
535
537
  ...withCodexCwdArg([...passthroughArgs, kickoff], process.cwd()),
536
538
  ];
@@ -36,7 +36,6 @@
36
36
  import { spawnSync } from 'node:child_process';
37
37
  import { realpathSync } from 'node:fs';
38
38
  import { sep, basename, dirname } from 'node:path';
39
- import { homedir } from 'node:os';
40
39
  import chalk from 'chalk';
41
40
  import {
42
41
  classifyDirty,
@@ -51,6 +50,7 @@ import {
51
50
  type ActiveCube,
52
51
  } from './cubes.js';
53
52
  import { defaultProbeSeat, type SeatStatus } from './seat-probe.js';
53
+ import { borgHomeRoot } from './private-root.js';
54
54
 
55
55
  // ------------------------------------------------------------------
56
56
  // Types
@@ -208,7 +208,7 @@ const defaultDeps: Required<CleanupDeps> = {
208
208
  const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
209
209
  return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
210
210
  },
211
- homeDir: () => homedir(),
211
+ homeDir: () => borgHomeRoot(),
212
212
  cwd: () => process.cwd(),
213
213
  listSeats: () => readAllProjectIdentities(),
214
214
  probeSeat: defaultProbeSeat,
@@ -1,8 +1,9 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import { chmod, copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
- import { homedir, tmpdir } from 'node:os';
3
+ import { tmpdir } from 'node:os';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import type { BorgCli } from './cubes.js';
6
+ import { borgHomeRoot } from './private-root.js';
6
7
 
7
8
  /**
8
9
  * Client #20: the narrow coordination surface a borg-launched agent needs
@@ -360,7 +361,7 @@ async function withNativeCodexProfileLayer<T>(
360
361
  env: NodeJS.ProcessEnv,
361
362
  query: (profileCwd: string, trustOverride: string) => Promise<CodexConfigSnapshot>
362
363
  ): Promise<T> {
363
- const codexHome = resolve(env.CODEX_HOME || join(homedir(), '.codex'));
364
+ const codexHome = resolve(env.CODEX_HOME || join(borgHomeRoot(), '.codex'));
364
365
  const profilePath = resolve(codexHome, `${profile}.config.toml`);
365
366
  if (dirname(profilePath) !== codexHome) {
366
367
  throw new Error('Codex profile path was invalid');
@@ -1,5 +1,4 @@
1
1
  import { mkdirSync, chmodSync, readdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
2
  import { join, resolve } from 'node:path';
4
3
  import { randomBytes } from 'node:crypto';
5
4
  import { spawn } from 'node:child_process';
@@ -11,7 +10,9 @@ import {
11
10
  BORG_CODEX_REMOTE_WAKE_ENV,
12
11
  codexAgentKindConfigArgs,
13
12
  codexRemoteWakeConfigArgs,
13
+ codexStateRootConfigArgs,
14
14
  } from './agent-runtime.js';
15
+ import { borgConfigRoot } from './private-root.js';
15
16
 
16
17
  /**
17
18
  * gh#528 — npm-managed Codex remote-wake.
@@ -75,7 +76,7 @@ export interface PrepareCodexRemoteDeps {
75
76
  isAlive?: (pid: number) => boolean;
76
77
  }
77
78
 
78
- export const DEFAULT_CODEX_REMOTE_DIR = join(homedir(), '.config', 'borgmcp', 'codex-remote');
79
+ export const DEFAULT_CODEX_REMOTE_DIR = join(borgConfigRoot(), 'codex-remote');
79
80
  const DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS = 30_000;
80
81
  const MAX_CODEX_APP_SERVER_STDERR_CHARS = 16_384;
81
82
 
@@ -426,6 +427,7 @@ export function defaultCodexRemoteDeps(): Pick<
426
427
  ...codexBorgSessionConfigArgs(),
427
428
  ...codexAgentKindConfigArgs(),
428
429
  ...codexRemoteWakeConfigArgs(),
430
+ ...codexStateRootConfigArgs(),
429
431
  ...codexAppServerSocketConfigArgs(socketPath),
430
432
  '--listen',
431
433
  `unix://${socketPath}`,