tmux-ide 2.9.0-beta.21 → 2.9.0-beta.22

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 (173) hide show
  1. package/bin/cli.js +52253 -49070
  2. package/package.json +1 -1
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
  4. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
  5. package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
  6. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
  7. package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
  8. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
  9. package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
  10. package/packages/contracts/src/actions-contract.ts +12 -0
  11. package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
  12. package/packages/contracts/src/daemon-wire.ts +10 -5
  13. package/packages/contracts/src/fleet-client-state.ts +3 -0
  14. package/packages/contracts/src/index.ts +2 -0
  15. package/packages/contracts/src/interaction-receipts.ts +39 -0
  16. package/packages/contracts/src/issue-error.ts +1 -1
  17. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  18. package/packages/contracts/src/pane-stream.ts +51 -8
  19. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  20. package/packages/contracts/src/window-links.ts +76 -0
  21. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  22. package/packages/core/src/interaction-receipts.ts +4 -0
  23. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  24. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  25. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  26. package/packages/daemon/dist/command-center/discovery.js +2 -9
  27. package/packages/daemon/dist/command-center/server.js +21 -1
  28. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  29. package/packages/daemon/dist/command-center/tmux-servers.js +345 -0
  30. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  31. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  32. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  33. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  34. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +16 -10
  35. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  36. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  37. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  38. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  39. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  40. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  41. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  42. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  43. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  44. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  46. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  47. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  48. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  49. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  50. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  51. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  52. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  53. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  54. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  55. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  56. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  57. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  58. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  59. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  60. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  61. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  62. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  63. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  64. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  65. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  66. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  67. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  68. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  70. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  71. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  72. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  73. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  74. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  75. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  76. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  77. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  78. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
  79. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  80. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  81. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  82. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  83. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  84. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  85. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +42 -1
  86. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  87. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  88. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  89. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  90. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  91. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  92. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  93. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  94. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  95. package/packages/daemon/src/command-center/discovery.ts +2 -10
  96. package/packages/daemon/src/command-center/server.ts +29 -1
  97. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  98. package/packages/daemon/src/command-center/tmux-servers.ts +468 -0
  99. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  100. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  101. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  102. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  103. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +16 -8
  104. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  105. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  106. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  107. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  108. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  109. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  110. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  111. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  112. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  113. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  114. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  115. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  116. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  117. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  118. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  119. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  120. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  121. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  122. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  123. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  124. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  125. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  126. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  127. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  128. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  129. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  130. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  131. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  132. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  133. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  134. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  135. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  136. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  137. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  138. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  139. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  140. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  141. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  142. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  143. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  144. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  145. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  146. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  147. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  148. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  149. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -36
  150. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  151. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  152. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  153. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  154. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
  155. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  156. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -0
  157. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  158. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  159. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  160. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +52 -0
  161. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  162. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  163. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  164. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  165. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  166. package/packages/daemon-client/package.json +3 -1
  167. package/packages/daemon-client/src/index.ts +2 -0
  168. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  169. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  170. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  171. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  172. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  173. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -22,6 +22,7 @@ export class FleetLifecycleAuthority {
22
22
  #startedAt;
23
23
  #registry;
24
24
  #runTmux;
25
+ #ensureChromeUpdater;
25
26
  #readFleet;
26
27
  #operations = new Map();
27
28
  #tail = Promise.resolve();
@@ -30,6 +31,7 @@ export class FleetLifecycleAuthority {
30
31
  this.#productVersion = options.productVersion;
31
32
  this.#startedAt = options.startedAt;
32
33
  this.#registry = options.registry;
34
+ this.#ensureChromeUpdater = options.ensureChromeUpdater ?? true;
33
35
  this.#runTmux = options.runTmux;
34
36
  this.#readFleet = options.readFleet ?? (() => readAdoptedFleet(this.#registry));
35
37
  }
@@ -94,11 +96,13 @@ export class FleetLifecycleAuthority {
94
96
  prepareTmuxTruecolorEnvironment(this.#runTmux, identity.sessionName);
95
97
  this.#runTmux(["set-environment", "-t", identity.sessionName, "TMUX_IDE", "1"]);
96
98
  this.#runTmux(adoptMarkArgv(identity.sessionName));
97
- try {
98
- this.#runTmux(updaterProbeArgv());
99
- }
100
- catch {
101
- this.#runTmux(updaterSpawnArgv());
99
+ if (this.#ensureChromeUpdater) {
100
+ try {
101
+ this.#runTmux(updaterProbeArgv());
102
+ }
103
+ catch {
104
+ this.#runTmux(updaterSpawnArgv());
105
+ }
102
106
  }
103
107
  if (!existing)
104
108
  this.#registry.add({
@@ -268,11 +272,13 @@ export class FleetLifecycleAuthority {
268
272
  this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_launch", input.command]);
269
273
  this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_hint", input.harness]);
270
274
  this.#runTmux(["select-pane", "-t", paneId, "-T", input.displayTitle]);
271
- try {
272
- this.#runTmux(updaterProbeArgv());
273
- }
274
- catch {
275
- this.#runTmux(updaterSpawnArgv());
275
+ if (this.#ensureChromeUpdater) {
276
+ try {
277
+ this.#runTmux(updaterProbeArgv());
278
+ }
279
+ catch {
280
+ this.#runTmux(updaterSpawnArgv());
281
+ }
276
282
  }
277
283
  }
278
284
  catch (error) {
@@ -1,3 +1,5 @@
1
+ import { assertCanonicalDaemonServerIntent } from "./canonical-daemon-bootstrap.js";
2
+ import { resolveTmuxServerIntent } from "./runtime-namespace.js";
1
3
  import { canonicalDaemonUrl, matchesCanonicalDaemonPredecessor, getCanonicalDaemonInfoPath, prepareCanonicalDaemonInfoForBootstrap, isCanonicalDaemonAlive, isCanonicalDaemonRecordOwnerProvenDead, probeCanonicalDaemonHealth, probeCanonicalDaemonIdentity, warnOnDaemonVersionSkew, } from "./canonical-daemon.js";
2
4
  import { DAEMON_WIRE_PROTOCOL_VERSION, isDaemonWireProtocolCompatible, } from "@tmux-ide/contracts";
3
5
  import { startEmbeddedDaemon, } from "./daemon-embed.js";
@@ -63,12 +65,15 @@ function assertIdentityMatches(info, identity) {
63
65
  }
64
66
  }
65
67
  async function assertAttachableDaemon(deps, info, options) {
66
- const identity = await deps.probeCanonicalDaemonIdentity(info);
68
+ const identity = await deps.probeCanonicalDaemonIdentity(info, undefined, Boolean(options.tmuxServerIntent));
67
69
  if (!identity) {
68
70
  throw new IdeError(`Canonical daemon PID ${info.pid} is alive but its identity endpoint is unavailable. ` +
69
71
  "Refusing takeover.", { code: "DAEMON_IDENTITY_UNAVAILABLE", exitCode: 1 });
70
72
  }
71
73
  assertIdentityMatches(info, identity);
74
+ await assertCanonicalDaemonServerIntent(info, options.tmuxServerIntent ?? null, {
75
+ identity: async () => identity,
76
+ });
72
77
  const health = await deps.probeCanonicalDaemonHealth(info);
73
78
  if (!health) {
74
79
  throw new IdeError(`Canonical daemon PID ${info.pid} is alive but its health endpoint is unavailable. ` +
@@ -180,6 +185,10 @@ async function waitForCanonicalWinner(deps, options, timeoutMs = 15_000) {
180
185
  * action converge on the same idempotent EmbeddedDaemonHandle.stop() path.
181
186
  */
182
187
  export async function runHeadlessDaemon(options = {}, deps = defaultDependencies) {
188
+ options = {
189
+ ...options,
190
+ tmuxServerIntent: options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent,
191
+ };
183
192
  let restoreTmuxWorkspaces = false;
184
193
  const lifecycle = {};
185
194
  while (true) {
@@ -212,6 +212,7 @@ export function resolveRuntimeNamespace(options = {}) {
212
212
  controlSocketPath: join(runtimeDir, "control.sock"),
213
213
  eventLogPath: join(stateHome, "events.jsonl"),
214
214
  tmuxSocket,
215
+ tmuxSocketExplicit: Boolean(tmuxSocketName || tmuxSocketPath),
215
216
  cleanupToken,
216
217
  namespaceId: development?.id ?? (isolated ? cleanupToken : "canonical"),
217
218
  persistence: isolated && !development ? "ephemeral" : "durable",
@@ -238,9 +239,11 @@ export function runtimeNamespaceEnvironment(namespace) {
238
239
  [STATE_HOME_ENV]: namespace.stateHome,
239
240
  [REGISTRY_DIR_ENV]: namespace.registryDir,
240
241
  [DAEMON_INFO_DIR_ENV]: namespace.daemonInfoDir,
241
- ...(namespace.tmuxSocket.kind === "name"
242
- ? { [TMUX_SOCKET_NAME_ENV]: namespace.tmuxSocket.name }
243
- : { [TMUX_SOCKET_PATH_ENV]: namespace.tmuxSocket.path }),
242
+ ...(namespace.tmuxSocketExplicit === false
243
+ ? {}
244
+ : namespace.tmuxSocket.kind === "name"
245
+ ? { [TMUX_SOCKET_NAME_ENV]: namespace.tmuxSocket.name }
246
+ : { [TMUX_SOCKET_PATH_ENV]: namespace.tmuxSocket.path }),
244
247
  ...(namespace.cleanupToken ? { [CLEANUP_TOKEN_ENV]: namespace.cleanupToken } : {}),
245
248
  });
246
249
  }
@@ -294,3 +297,14 @@ export function assertQualifiedDevelopmentLaunch() {
294
297
  if (resolveRuntimeNamespace().development)
295
298
  throw new Error("Development launch requires exact build artifacts (D03); installed CLI fallback is disabled");
296
299
  }
300
+ /** Caller intent differs from the default used when starting a new owner. */
301
+ export function resolveTmuxServerIntent(options = {}) {
302
+ const env = options.env ?? process.env;
303
+ const namespace = resolveRuntimeNamespace(options);
304
+ if (namespace.isolated ||
305
+ nonEmpty(env, TMUX_SOCKET_NAME_ENV) ||
306
+ nonEmpty(env, TMUX_SOCKET_PATH_ENV))
307
+ return { source: "explicit", selector: namespace.tmuxSocket };
308
+ const current = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX ?? "")?.[1];
309
+ return current ? { source: "current", selector: { kind: "path", path: current } } : null;
310
+ }
@@ -0,0 +1,59 @@
1
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
+ import { delimiter, isAbsolute, join } from "node:path";
3
+ import { resolveRuntimeNamespace } from "./runtime-namespace.js";
4
+ import { resolveBundledTmux } from "./bundled-tmux.js";
5
+ export function resolveTmuxExecutable() {
6
+ if (resolveRuntimeNamespace().development)
7
+ return resolveBundledTmux();
8
+ const configured = process.env.TMUX_IDE_TMUX_BIN;
9
+ if (!configured) {
10
+ const bundled = resolveBundledTmux();
11
+ if (bundled)
12
+ return bundled;
13
+ }
14
+ const candidates = configured
15
+ ? [configured]
16
+ : (process.env.PATH ?? "")
17
+ .split(delimiter)
18
+ // Empty and relative PATH entries mean the daemon/project cwd. They
19
+ // are never executable authority for a privileged tmux mutation.
20
+ .filter((entry) => entry.length > 0 && isAbsolute(entry))
21
+ .map((entry) => join(entry, "tmux"));
22
+ for (const candidate of candidates) {
23
+ try {
24
+ if (!isAbsolute(candidate))
25
+ continue;
26
+ accessSync(candidate, constants.X_OK);
27
+ const canonical = realpathSync(candidate);
28
+ if (statSync(canonical).isFile())
29
+ return canonical;
30
+ }
31
+ catch {
32
+ // Continue to the next daemon-start candidate.
33
+ }
34
+ }
35
+ throw new Error("tmux_executable_unavailable");
36
+ }
37
+ const SAFE_TERMINAL_VALUE = /^(?:xterm|screen|tmux|rxvt|vt100|ansi)[A-Za-z0-9+._-]{0,58}$/u;
38
+ const SAFE_LOCALE_VALUE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}$/u;
39
+ /**
40
+ * The pinned tmux client receives only presentation metadata. Everything else
41
+ * is denied by omission: PATH/HOME/SHELL, TMUX/TMUX_PANE/TMUX_TMPDIR, shell
42
+ * startup hooks, dynamic-loader variables, Node options, and project secrets
43
+ * cannot redirect authority or inject code into this daemon-owned execution.
44
+ */
45
+ export function tmuxClientEnvironment(source) {
46
+ const environment = {
47
+ TERM: SAFE_TERMINAL_VALUE.test(source.TERM ?? "") ? source.TERM : "xterm-256color",
48
+ // A pinned runner may be the first tmux client and therefore create the
49
+ // server. Never let a headless parent (`TERM=dumb`, `NO_COLOR=1`) become
50
+ // the global environment inherited by every later interactive child.
51
+ COLORTERM: "truecolor",
52
+ };
53
+ for (const name of ["LANG", "LC_ALL", "LC_CTYPE"]) {
54
+ const value = source[name];
55
+ if (value && SAFE_LOCALE_VALUE.test(value))
56
+ environment[name] = value;
57
+ }
58
+ return environment;
59
+ }
@@ -63,3 +63,7 @@ export function createNamedSocketFence(authority, executable, environment) {
63
63
  isPinned: () => shared.identity !== null,
64
64
  };
65
65
  }
66
+ /** True only before this exact authority has observed any live socket. */
67
+ export function isNamedSocketAuthorityUnbound(authority) {
68
+ return authority.socketSelector.kind === "name" && !states.get(authority)?.identity;
69
+ }
@@ -0,0 +1,62 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createPinnedWorkspaceTmuxRunner, createPinnedWorkspaceTmuxAsyncRunner, } from "./workspace-pane-creation.js";
3
+ import { shellEscape } from "./shell.js";
4
+ /** Guard each command on the server which actually accepted this connection. */
5
+ export function fenceNativeTmuxCommand(args, expected) {
6
+ const { pid, startTime } = expected;
7
+ if (!/^[1-9][0-9]*$/u.test(pid) || !/^[1-9][0-9]*$/u.test(startTime))
8
+ throw new Error("Invalid native tmux server identity");
9
+ if (args.length === 0 || args.some((arg) => arg.includes("\0")))
10
+ throw new Error("Invalid tmux command");
11
+ const globalFlags = [];
12
+ let firstCommand = 0;
13
+ while (args[firstCommand]?.startsWith("-")) {
14
+ const flag = args[firstCommand];
15
+ if (flag !== "-N" && flag !== "-u")
16
+ throw new Error("Unsupported tmux global option in generation-fenced command");
17
+ globalFlags.push(flag);
18
+ firstCommand += 1;
19
+ }
20
+ if (firstCommand === args.length)
21
+ throw new Error("Missing tmux command");
22
+ const condition = `#{&&:#{==:#{pid},${pid}},#{==:#{start_time},${startTime}}}`;
23
+ // Preserve arbitrary literal payloads; a standalone ; is the native argv
24
+ // command separator already used by the existing runners.
25
+ const command = args
26
+ .slice(firstCommand)
27
+ .map((arg) => (arg === ";" ? ";" : shellEscape(arg)))
28
+ .join(" ");
29
+ const refusal = `tmux-server-stale.${randomUUID()}`;
30
+ return {
31
+ argv: [...globalFlags, "if-shell", "-F", condition, command, `display-message -p '${refusal}'`],
32
+ verify(output) {
33
+ if (output.trimEnd() === refusal)
34
+ throw new Error("Tmux server generation changed before command execution");
35
+ return output;
36
+ },
37
+ };
38
+ }
39
+ /** Socket validation alone leaves a validation-to-connect replacement race. */
40
+ export function createServerGenerationFencedTmuxRunner(authority, expected) {
41
+ const run = createPinnedWorkspaceTmuxRunner(authority, { timeoutMs: 5_000 });
42
+ const [pid, startTime] = expected
43
+ ? [expected.pid, expected.startTime]
44
+ : run(["display-message", "-p", "#{pid}\t#{start_time}"]).split("\t");
45
+ if (!pid || !startTime)
46
+ throw new Error("Invalid native tmux server identity");
47
+ const identity = { pid, startTime };
48
+ // Validate once before publishing an owner; each command rechecks natively.
49
+ fenceNativeTmuxCommand(["display-message", "-p", ""], identity);
50
+ return (args) => {
51
+ const command = fenceNativeTmuxCommand(args, identity);
52
+ return command.verify(run(command.argv));
53
+ };
54
+ }
55
+ export function createServerGenerationFencedTmuxAsyncRunner(authority, expected) {
56
+ const run = createPinnedWorkspaceTmuxAsyncRunner(authority);
57
+ fenceNativeTmuxCommand(["display-message", "-p", ""], expected);
58
+ return async (args, signal) => {
59
+ const command = fenceNativeTmuxCommand(args, expected);
60
+ return command.verify(await run(command.argv, signal));
61
+ };
62
+ }
@@ -0,0 +1,285 @@
1
+ import { createNativeTmuxSessionCreator } from "./tmux-server-session-create.js";
2
+ import { createTmuxSessionMutationFence } from "./tmux-session-mutation-fence.js";
3
+ import { createNativeTmuxSessionOpener } from "./tmux-server-session-open.js";
4
+ import { randomUUID } from "node:crypto";
5
+ import { mkdirSync } from "node:fs";
6
+ import { z } from "zod";
7
+ import { isVisibleFleetSession } from "../command-center/discovery.js";
8
+ import { WorkspaceTerminalInventoryRuntime } from "../terminal/attachments/native-runtime.js";
9
+ import { createTmuxAgentStatusProbe } from "../terminal/attachments/agent-status-probe.js";
10
+ import { createPaneStreamRuntime } from "../terminal/pane-stream/runtime.js";
11
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
12
+ import { SessionRuntimeRegistry } from "../terminal/session-runtime/registry.js";
13
+ import { createSessionRuntimeMultiplexerBackend } from "../terminal/session-runtime/multiplexer-backend.js";
14
+ import { TmuxExternalInteractionObserver } from "./tmux-external-interaction-observer.js";
15
+ import { WorkspaceMultiplexerAuthority } from "./workspace-multiplexer-verbs.js";
16
+ import { WorkspacePaneCreationAuthority, } from "./workspace-pane-creation.js";
17
+ import { WorkspaceRegistry } from "./workspace-registry.js";
18
+ import { createServerGenerationFencedTmuxRunner, createServerGenerationFencedTmuxAsyncRunner, } from "./tmux-server-generation-runner.js";
19
+ /** Refresh discovered live membership without changing durable workspace intent or aliases. */
20
+ export function createNativeTmuxServerCatalog(workspaceRegistry, run, assertOpen = () => undefined) {
21
+ return async () => {
22
+ assertOpen();
23
+ const raw = await run([
24
+ "list-panes",
25
+ "-a",
26
+ "-F",
27
+ "#{pid}\t#{session_id}\t#{session_created}\t#{session_name}\t#{pane_id}\t#{session_path}",
28
+ ], AbortSignal.timeout(2_000));
29
+ assertOpen();
30
+ const rows = new Map();
31
+ for (const line of raw.trim().split("\n")) {
32
+ const [pid, id, created, name, pane, dir] = line.split("\t");
33
+ if (!pid ||
34
+ !/^\d+$/.test(pid) ||
35
+ !id ||
36
+ !/^\$\d+$/.test(id) ||
37
+ !created ||
38
+ !/^\d+$/.test(created) ||
39
+ !name ||
40
+ !pane ||
41
+ !/^%\d+$/.test(pane) ||
42
+ !dir)
43
+ throw new Error("Invalid native server catalog");
44
+ if (!isVisibleFleetSession(name))
45
+ continue;
46
+ const liveSessionId = liveSessionIdForNativeIdentity(pid, id, created);
47
+ const row = rows.get(liveSessionId) ?? {
48
+ summary: { liveSessionId, sessionName: name, paneCount: 0 },
49
+ panes: new Set(),
50
+ dir,
51
+ };
52
+ row.panes.add(pane);
53
+ rows.set(liveSessionId, row);
54
+ }
55
+ const names = new Set([...rows.values()].map(({ summary }) => summary.sessionName));
56
+ for (const workspace of workspaceRegistry.list())
57
+ if (workspaceRegistry.isVolatile(workspace.name) && !names.has(workspace.sessionName))
58
+ workspaceRegistry.remove(workspace.name);
59
+ for (const { summary, dir } of rows.values())
60
+ if (!workspaceRegistry.has(summary.sessionName) &&
61
+ !workspaceRegistry.list().some((workspace) => workspace.sessionName === summary.sessionName))
62
+ workspaceRegistry.add({
63
+ name: summary.sessionName,
64
+ sessionName: summary.sessionName,
65
+ projectDir: dir,
66
+ persistence: "volatile",
67
+ });
68
+ return [...rows.values()].map(({ summary, panes }) => ({ ...summary, paneCount: panes.size }));
69
+ };
70
+ }
71
+ /** One proven server incarnation. Never elects an endpoint or changes ambient tmux authority. */
72
+ export async function createNativeTmuxServerOwner(options) {
73
+ const generation = z.uuid().parse(options.generation);
74
+ const authority = options.tmuxAuthority;
75
+ if (authority.socketSelector.kind !== "path") {
76
+ throw new Error("A server owner requires a proven direct socket authority");
77
+ }
78
+ const socketPath = authority.socketSelector.path;
79
+ mkdirSync(options.stateDirectory, { recursive: true, mode: 0o700 });
80
+ const workspaceRegistry = new WorkspaceRegistry({
81
+ dir: options.stateDirectory,
82
+ listSessions: () => [],
83
+ });
84
+ const sessionMutationFence = createTmuxSessionMutationFence();
85
+ const generationRun = sessionMutationFence.wrap(createServerGenerationFencedTmuxRunner(authority, options.nativeServerIdentity));
86
+ const identityParts = generationRun(["display-message", "-p", "#{pid}\t#{start_time}"]).split("\t");
87
+ const nativeServerIdentity = options.nativeServerIdentity ?? {
88
+ pid: identityParts[0],
89
+ startTime: identityParts[1],
90
+ };
91
+ const generationRunAsync = createServerGenerationFencedTmuxAsyncRunner(authority, nativeServerIdentity);
92
+ const multiplexer = new WorkspaceMultiplexerAuthority({
93
+ daemonInstanceId: generation,
94
+ registry: workspaceRegistry,
95
+ tmuxAuthority: authority,
96
+ io: { runTmux: generationRun },
97
+ });
98
+ const paneCreation = new WorkspacePaneCreationAuthority({
99
+ daemonInstanceId: generation,
100
+ registry: workspaceRegistry,
101
+ tmuxAuthority: authority,
102
+ io: { runTmux: generationRun },
103
+ });
104
+ let disposed = false;
105
+ let disposePromise = null;
106
+ let observerStarted = null;
107
+ let receiptSequence = 0;
108
+ const assertOpen = () => {
109
+ if (disposed)
110
+ throw new Error("Tmux server owner is retired");
111
+ };
112
+ const sessionRuntimeRegistry = new SessionRuntimeRegistry({
113
+ generation,
114
+ semanticMutations: {
115
+ resolveSession: (name) => workspaceRegistry.get(name)?.sessionName ?? null,
116
+ execute: (operationId, intent, timing) => {
117
+ assertOpen();
118
+ if (intent.verb === "workspace.pane.read")
119
+ return multiplexer.readPane(operationId, intent);
120
+ if (intent.verb === "workspace.window.link.select" ||
121
+ intent.verb === "workspace.window.link.unlink" ||
122
+ intent.verb === "workspace.pane.select") {
123
+ return multiplexer.mutateWindowLink({ operationId, expectedDaemonInstanceId: generation, intent }, (session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action));
124
+ }
125
+ return multiplexer.mutate({ operationId, expectedDaemonInstanceId: generation, intent }, timing);
126
+ },
127
+ publishReceipt: (receipt) => ({
128
+ ...receipt,
129
+ type: "interaction.receipt",
130
+ sequence: ++receiptSequence,
131
+ }),
132
+ traceAuthority: { generation, incarnation: null },
133
+ },
134
+ mirror: {
135
+ executable: authority.executablePath,
136
+ socketPath,
137
+ resolveSocketPath: () => generationRun(["display-message", "-p", "#{socket_path}"]),
138
+ nativeServerIdentity,
139
+ internalReadHookEmission: (pane, marker) => observer.internalReadHookEmission(pane, marker),
140
+ },
141
+ });
142
+ const terminalInventoryRuntime = new WorkspaceTerminalInventoryRuntime({
143
+ registry: workspaceRegistry,
144
+ sessionRuntimeRegistry,
145
+ tmuxAuthority: { ...authority, trustedCwd: options.stateDirectory, nativeServerIdentity },
146
+ agentStatusProbeFactory: ({ run }) => createTmuxAgentStatusProbe({ run }),
147
+ onInventory: (snapshot) => multiplexer.adoptPaneInventory(snapshot.panes),
148
+ onSessionInventory: (session, snapshot) => multiplexer.adoptSessionPaneInventory(session, snapshot?.panes ?? []),
149
+ });
150
+ const observer = new TmuxExternalInteractionObserver({
151
+ daemonInstanceId: generation,
152
+ internalReadOwnerToken: randomUUID(),
153
+ registry: workspaceRegistry,
154
+ tmuxAuthority: authority,
155
+ io: { runTmux: generationRunAsync },
156
+ onGap: () => terminalInventoryRuntime.invalidate(),
157
+ onObserved: ({ workspaceName, semanticPaneId, operationKind, operationId }) => operationId !== null &&
158
+ sessionRuntimeRegistry.observeTmuxInteraction({
159
+ workspaceName,
160
+ semanticPaneId,
161
+ operationKind,
162
+ operationId,
163
+ }),
164
+ });
165
+ const paneStreamRuntime = createPaneStreamRuntime({
166
+ daemonInstanceId: generation,
167
+ webSocketUrl: options.webSocketUrl,
168
+ sessionRuntimeRegistry,
169
+ semanticPaneCatalog: terminalInventoryRuntime.semanticPaneCatalog,
170
+ });
171
+ const backend = createSessionRuntimeMultiplexerBackend({
172
+ registry: sessionRuntimeRegistry,
173
+ resolveSession: (name) => workspaceRegistry.get(name)?.sessionName ?? null,
174
+ });
175
+ const catalog = createNativeTmuxServerCatalog(workspaceRegistry, generationRunAsync, assertOpen);
176
+ const sessionCreator = createNativeTmuxSessionCreator({
177
+ generation,
178
+ registry: workspaceRegistry,
179
+ run: generationRun,
180
+ assertOpen,
181
+ });
182
+ const sessionOpener = createNativeTmuxSessionOpener({
183
+ generation,
184
+ registry: workspaceRegistry,
185
+ run: generationRunAsync,
186
+ assertOpen,
187
+ });
188
+ const openSession = async (liveSessionId) => {
189
+ await catalog();
190
+ const result = await sessionOpener.openSession(liveSessionId);
191
+ terminalInventoryRuntime.invalidate();
192
+ return result;
193
+ };
194
+ const multiplexerBackend = {
195
+ mutate: async (...args) => {
196
+ assertOpen();
197
+ if (args[0].expectedDaemonInstanceId !== generation)
198
+ throw new Error("Tmux server generation mismatch");
199
+ await catalog();
200
+ observerStarted ??= observer.start();
201
+ await observerStarted;
202
+ assertOpen();
203
+ return backend.mutate(...args);
204
+ },
205
+ };
206
+ const dispose = () => {
207
+ if (disposePromise)
208
+ return disposePromise;
209
+ disposed = true;
210
+ disposePromise = (async () => {
211
+ await sessionOpener.dispose();
212
+ await sessionCreator.dispose();
213
+ await paneCreation.dispose();
214
+ try {
215
+ await paneStreamRuntime.dispose();
216
+ }
217
+ finally {
218
+ try {
219
+ await observer.dispose();
220
+ }
221
+ finally {
222
+ terminalInventoryRuntime.dispose();
223
+ try {
224
+ await multiplexer.dispose();
225
+ }
226
+ finally {
227
+ await sessionRuntimeRegistry.dispose();
228
+ }
229
+ }
230
+ }
231
+ })();
232
+ return disposePromise;
233
+ };
234
+ try {
235
+ await terminalInventoryRuntime.whenReady();
236
+ await catalog();
237
+ }
238
+ catch (error) {
239
+ await dispose();
240
+ throw error;
241
+ }
242
+ return {
243
+ serverId: options.serverId,
244
+ generation,
245
+ catalog,
246
+ openSession,
247
+ createSession: sessionCreator.createSession,
248
+ createSessionPane: async (liveSessionId, request) => {
249
+ assertOpen();
250
+ await catalog();
251
+ const sessionName = workspaceRegistry.get(request.intent.workspaceName)?.sessionName;
252
+ if (!sessionName)
253
+ throw new Error("Selected workspace is unavailable");
254
+ const result = await sessionMutationFence.execute({
255
+ liveSessionId,
256
+ sessionName,
257
+ run: generationRunAsync,
258
+ mutate: () => paneCreation.create(request),
259
+ });
260
+ terminalInventoryRuntime.invalidate();
261
+ return result;
262
+ },
263
+ mutateSession: async (liveSessionId, request, authenticatedHostClientId) => {
264
+ assertOpen();
265
+ await catalog();
266
+ const sessionName = workspaceRegistry.get(request.intent.workspaceName)?.sessionName;
267
+ if (!sessionName)
268
+ throw new Error("Selected workspace is unavailable");
269
+ observerStarted ??= observer.start();
270
+ await observerStarted;
271
+ return sessionMutationFence.execute({
272
+ liveSessionId,
273
+ sessionName,
274
+ run: generationRunAsync,
275
+ mutate: () => multiplexerBackend.mutate(request, authenticatedHostClientId, undefined, true),
276
+ });
277
+ },
278
+ workspaceRegistry,
279
+ multiplexerBackend,
280
+ sessionRuntimeRegistry,
281
+ terminalInventoryRuntime,
282
+ paneStreamRuntime,
283
+ dispose,
284
+ };
285
+ }