tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +46791 -43589
- package/package.json +2 -2
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
- package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
- package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
- package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
- package/packages/contracts/src/actions-contract.ts +12 -0
- package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
- package/packages/contracts/src/daemon-wire.ts +10 -5
- package/packages/contracts/src/fleet-client-state.ts +3 -0
- package/packages/contracts/src/fleet-lifecycle.ts +5 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/interaction-receipts.ts +39 -0
- package/packages/contracts/src/issue-error.ts +1 -1
- package/packages/contracts/src/multiplexer-verbs.ts +25 -1
- package/packages/contracts/src/pane-stream.ts +51 -8
- package/packages/contracts/src/tmux-server-scope.ts +172 -0
- package/packages/contracts/src/window-links.ts +76 -0
- package/packages/contracts/src/workspace-multiplexer.ts +54 -0
- package/packages/core/src/interaction-receipts.ts +4 -0
- package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
- package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
- package/packages/daemon/dist/command-center/actions/registry.js +13 -1
- package/packages/daemon/dist/command-center/discovery.js +2 -9
- package/packages/daemon/dist/command-center/server.js +21 -1
- package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
- package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
- package/packages/daemon/dist/lib/daemon-embed.js +198 -30
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
- package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
- package/packages/daemon/dist/lib/headless-daemon.js +10 -1
- package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
- package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
- package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
- package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
- package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
- package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
- package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
- package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
- package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
- package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
- package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
- package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
- package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
- package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
- package/packages/daemon/dist/lib/workspace-registry.js +4 -0
- package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
- package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
- package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
- package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
- package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
- package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
- package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
- package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
- package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
- package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
- package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
- package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
- package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
- package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
- package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
- package/packages/daemon/src/command-center/actions/errors.ts +4 -0
- package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
- package/packages/daemon/src/command-center/actions/registry.ts +14 -0
- package/packages/daemon/src/command-center/discovery.ts +2 -10
- package/packages/daemon/src/command-center/server.ts +29 -1
- package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
- package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
- package/packages/daemon/src/lib/daemon-embed.ts +241 -28
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
- package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
- package/packages/daemon/src/lib/headless-daemon.ts +18 -1
- package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
- package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
- package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
- package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
- package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
- package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
- package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
- package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
- package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
- package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
- package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
- package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
- package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
- package/packages/daemon/src/lib/workspace-registry.ts +5 -0
- package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
- package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
- package/packages/daemon/src/terminal/mirror/events.ts +2 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
- package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
- package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
- package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
- package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
- package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
- package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
- package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
- package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
- package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
- package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
- package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
- package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
- package/packages/daemon-client/package.json +3 -1
- package/packages/daemon-client/src/index.ts +2 -0
- package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
- package/packages/daemon-client/src/pane-stream-client.ts +26 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
- package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
- package/packages/daemon-client/src/tmux-server-client.ts +287 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { TmuxServerIdSchemaZ, TmuxServerRegistrationRequestSchemaZ, TmuxServerScopeSchemaZ, } from "@tmux-ide/contracts";
|
|
3
|
+
// Admission limits, not performance claims. Discovery is explicit/on-demand.
|
|
4
|
+
export const MAX_TMUX_SERVER_OWNERS = 16;
|
|
5
|
+
export const TmuxServerSelectorSchemaZ = TmuxServerRegistrationRequestSchemaZ.shape.selector;
|
|
6
|
+
export const TmuxServerRegistrationSchemaZ = TmuxServerRegistrationRequestSchemaZ.extend({
|
|
7
|
+
serverId: TmuxServerIdSchemaZ,
|
|
8
|
+
}).strict();
|
|
9
|
+
export class TmuxServerScopeError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code) {
|
|
12
|
+
super(`tmux server ${code}`);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "TmuxServerScopeError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** One process; no shared mutable tmux authority, runtime cache or retry ledger. */
|
|
18
|
+
export class TmuxServerOwners {
|
|
19
|
+
options;
|
|
20
|
+
#entries = new Map();
|
|
21
|
+
#tail = Promise.resolve();
|
|
22
|
+
#disposed = false;
|
|
23
|
+
#disposePromise = null;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
}
|
|
27
|
+
#serial(work) {
|
|
28
|
+
const run = this.#tail.then(() => {
|
|
29
|
+
if (this.#disposed)
|
|
30
|
+
throw new TmuxServerScopeError("disposed");
|
|
31
|
+
return work();
|
|
32
|
+
});
|
|
33
|
+
this.#tail = run.catch(() => undefined);
|
|
34
|
+
return run;
|
|
35
|
+
}
|
|
36
|
+
#descriptor(entry) {
|
|
37
|
+
return entry.scope
|
|
38
|
+
? { ...entry.scope, label: entry.registration.label, state: "online" }
|
|
39
|
+
: {
|
|
40
|
+
serverId: entry.registration.serverId,
|
|
41
|
+
generation: null,
|
|
42
|
+
label: entry.registration.label,
|
|
43
|
+
state: "offline",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
list() {
|
|
47
|
+
return [...this.#entries.values()].map((entry) => this.#descriptor(entry));
|
|
48
|
+
}
|
|
49
|
+
registrations() {
|
|
50
|
+
return [...this.#entries.values()].map((entry) => structuredClone(entry.registration));
|
|
51
|
+
}
|
|
52
|
+
#save() {
|
|
53
|
+
this.options.persist?.(this.registrations());
|
|
54
|
+
}
|
|
55
|
+
async #retire(entry) {
|
|
56
|
+
const owner = entry.owner;
|
|
57
|
+
entry.owner = null;
|
|
58
|
+
entry.scope = null;
|
|
59
|
+
entry.observation = null;
|
|
60
|
+
if (owner)
|
|
61
|
+
await owner.dispose();
|
|
62
|
+
}
|
|
63
|
+
async #refresh(entry, observed) {
|
|
64
|
+
const next = observed === undefined ? await this.options.probe(entry.registration.selector) : observed;
|
|
65
|
+
if (next && entry.observation?.fingerprint === next.fingerprint && entry.observation.valid())
|
|
66
|
+
return;
|
|
67
|
+
// Absence revokes command/stream leases without claiming native server death.
|
|
68
|
+
// Socket recreation also needs a fresh connection authority, even if PID survived.
|
|
69
|
+
await this.#retire(entry);
|
|
70
|
+
if (this.#disposed || !next || !next.valid())
|
|
71
|
+
return;
|
|
72
|
+
const duplicate = [...this.#entries.values()].find((other) => other !== entry &&
|
|
73
|
+
other.observation?.fingerprint === next.fingerprint &&
|
|
74
|
+
other.observation.valid());
|
|
75
|
+
if (duplicate)
|
|
76
|
+
return; // Never construct a second owner for a proven live alias.
|
|
77
|
+
const scope = { serverId: entry.registration.serverId, generation: randomUUID() };
|
|
78
|
+
let owner;
|
|
79
|
+
try {
|
|
80
|
+
owner = await this.options.create(entry.registration, scope, next);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return;
|
|
84
|
+
} // Registration remains visible/offline; explicit refresh retries.
|
|
85
|
+
if (this.#disposed || !next.valid()) {
|
|
86
|
+
await owner.dispose();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
entry.observation = next;
|
|
90
|
+
entry.scope = scope;
|
|
91
|
+
entry.owner = owner;
|
|
92
|
+
}
|
|
93
|
+
register(input) {
|
|
94
|
+
return this.#serial(async () => {
|
|
95
|
+
const registration = TmuxServerRegistrationSchemaZ.parse({
|
|
96
|
+
...input,
|
|
97
|
+
serverId: input.serverId ?? `tmux-server.${randomUUID().replaceAll("-", "")}`,
|
|
98
|
+
});
|
|
99
|
+
const sameSelector = [...this.#entries.values()].find((entry) => JSON.stringify(entry.registration.selector) === JSON.stringify(registration.selector));
|
|
100
|
+
if (sameSelector) {
|
|
101
|
+
await this.#refresh(sameSelector);
|
|
102
|
+
return this.#descriptor(sameSelector);
|
|
103
|
+
}
|
|
104
|
+
if (this.#entries.has(registration.serverId))
|
|
105
|
+
throw new TypeError("Server registration already exists");
|
|
106
|
+
const observation = await this.options.probe(registration.selector);
|
|
107
|
+
if (this.#disposed)
|
|
108
|
+
throw new TmuxServerScopeError("disposed");
|
|
109
|
+
const alias = observation &&
|
|
110
|
+
[...this.#entries.values()].find((entry) => entry.observation?.fingerprint === observation.fingerprint && entry.observation.valid());
|
|
111
|
+
if (alias)
|
|
112
|
+
return this.#descriptor(alias);
|
|
113
|
+
if (this.#entries.size >= MAX_TMUX_SERVER_OWNERS)
|
|
114
|
+
throw new TmuxServerScopeError("capacity");
|
|
115
|
+
const entry = { registration, observation: null, scope: null, owner: null };
|
|
116
|
+
this.#entries.set(registration.serverId, entry);
|
|
117
|
+
try {
|
|
118
|
+
this.#save();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.#entries.delete(registration.serverId);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
await this.#refresh(entry, observation);
|
|
125
|
+
return this.#descriptor(entry);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Adopt the existing default owner rather than creating a duplicate mirror. */
|
|
129
|
+
adopt(registrationInput, scopeInput, observation, owner) {
|
|
130
|
+
const registration = TmuxServerRegistrationSchemaZ.parse(registrationInput);
|
|
131
|
+
const scope = TmuxServerScopeSchemaZ.parse(scopeInput);
|
|
132
|
+
if (scope.serverId !== registration.serverId || this.#entries.size || this.#disposed)
|
|
133
|
+
throw new TypeError("Invalid default owner adoption");
|
|
134
|
+
this.#entries.set(scope.serverId, { registration, scope, observation, owner });
|
|
135
|
+
}
|
|
136
|
+
refresh() {
|
|
137
|
+
return this.#serial(async () => {
|
|
138
|
+
// Sequential admission bounds subprocess work and avoids alias races.
|
|
139
|
+
for (const entry of this.#entries.values())
|
|
140
|
+
await this.#refresh(entry);
|
|
141
|
+
return this.list();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
current(scopeInput) {
|
|
145
|
+
const scope = TmuxServerScopeSchemaZ.parse(scopeInput);
|
|
146
|
+
if (this.#disposed)
|
|
147
|
+
throw new TmuxServerScopeError("disposed");
|
|
148
|
+
const entry = this.#entries.get(scope.serverId);
|
|
149
|
+
if (!entry)
|
|
150
|
+
throw new TmuxServerScopeError("not-found");
|
|
151
|
+
if (!entry.scope || !entry.owner)
|
|
152
|
+
throw new TmuxServerScopeError("offline");
|
|
153
|
+
if (entry.scope.generation !== scope.generation || !entry.observation?.valid())
|
|
154
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
155
|
+
return entry.owner;
|
|
156
|
+
}
|
|
157
|
+
async withOwner(scope, work) {
|
|
158
|
+
const owner = this.current(scope);
|
|
159
|
+
const result = await work(owner);
|
|
160
|
+
if (this.current(scope) !== owner)
|
|
161
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
remove(serverId) {
|
|
165
|
+
return this.#serial(async () => {
|
|
166
|
+
const entry = this.#entries.get(TmuxServerIdSchemaZ.parse(serverId));
|
|
167
|
+
if (!entry)
|
|
168
|
+
throw new TmuxServerScopeError("not-found");
|
|
169
|
+
this.#entries.delete(serverId);
|
|
170
|
+
try {
|
|
171
|
+
this.#save();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
this.#entries.set(serverId, entry);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
await this.#retire(entry); // never kill-server or kill-session
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
dispose() {
|
|
181
|
+
if (this.#disposePromise)
|
|
182
|
+
return this.#disposePromise;
|
|
183
|
+
this.#disposed = true;
|
|
184
|
+
this.#disposePromise = (async () => {
|
|
185
|
+
await this.#tail;
|
|
186
|
+
const entries = [...this.#entries.values()];
|
|
187
|
+
this.#entries.clear();
|
|
188
|
+
await Promise.allSettled(entries.map((entry) => this.#retire(entry)));
|
|
189
|
+
})();
|
|
190
|
+
return this.#disposePromise;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isNamedSocketAuthorityUnbound } from "./tmux-named-socket-fence.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { captureUnixSocketIdentity, revalidateUnixSocketIdentity, } from "./unix-socket-authority.js";
|
|
5
|
+
/** Two read-only, no-start queries fence the socket and server incarnation. */
|
|
6
|
+
export function captureTmuxServerProof(run) {
|
|
7
|
+
const args = ["-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"];
|
|
8
|
+
try {
|
|
9
|
+
const raw = run(args).trimEnd();
|
|
10
|
+
const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(raw);
|
|
11
|
+
if (!match || raw.length > 8192)
|
|
12
|
+
return null;
|
|
13
|
+
const socket = captureUnixSocketIdentity(match[1]);
|
|
14
|
+
if (run(args).trimEnd() !== raw)
|
|
15
|
+
return null;
|
|
16
|
+
revalidateUnixSocketIdentity(socket);
|
|
17
|
+
return {
|
|
18
|
+
version: 1,
|
|
19
|
+
kind: "live",
|
|
20
|
+
digest: createHash("sha256")
|
|
21
|
+
.update(JSON.stringify([socket.path, match[2], match[3]]))
|
|
22
|
+
.digest("hex"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function captureTmuxServerProofAsync(run) {
|
|
30
|
+
const args = ["-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"];
|
|
31
|
+
try {
|
|
32
|
+
const raw = (await run(args)).trimEnd();
|
|
33
|
+
const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(raw);
|
|
34
|
+
if (!match || raw.length > 8192)
|
|
35
|
+
return null;
|
|
36
|
+
const socket = captureUnixSocketIdentity(match[1]);
|
|
37
|
+
if ((await run(args)).trimEnd() !== raw)
|
|
38
|
+
return null;
|
|
39
|
+
revalidateUnixSocketIdentity(socket);
|
|
40
|
+
return {
|
|
41
|
+
version: 1,
|
|
42
|
+
kind: "live",
|
|
43
|
+
digest: createHash("sha256")
|
|
44
|
+
.update(JSON.stringify([socket.path, match[2], match[3]]))
|
|
45
|
+
.digest("hex"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** A selector commitment is not evidence of an absent or live server. */
|
|
53
|
+
export function captureUnboundTmuxSelectorProof(authority) {
|
|
54
|
+
if (!isNamedSocketAuthorityUnbound(authority) || authority.socketSelector.kind !== "name")
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const executable = realpathSync(authority.executablePath);
|
|
58
|
+
const stat = statSync(executable, { bigint: true });
|
|
59
|
+
const uid = process.getuid?.();
|
|
60
|
+
if (uid === undefined || !stat.isFile())
|
|
61
|
+
return null;
|
|
62
|
+
return {
|
|
63
|
+
version: 1,
|
|
64
|
+
kind: "unbound-name",
|
|
65
|
+
digest: createHash("sha256")
|
|
66
|
+
.update(JSON.stringify([
|
|
67
|
+
"sanitized-tmux-environment-v1",
|
|
68
|
+
executable,
|
|
69
|
+
String(stat.dev),
|
|
70
|
+
String(stat.ino),
|
|
71
|
+
String(stat.mtimeNs),
|
|
72
|
+
String(stat.size),
|
|
73
|
+
uid,
|
|
74
|
+
authority.socketSelector.name,
|
|
75
|
+
]))
|
|
76
|
+
.digest("hex"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, realpathSync, unlinkSync, } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { tmuxClientEnvironment } from "./tmux-client-execution.js";
|
|
8
|
+
import { captureUnixSocketIdentity, revalidateUnixSocketIdentity, } from "./unix-socket-authority.js";
|
|
9
|
+
import { MAX_TMUX_SERVER_OWNERS, TmuxServerRegistrationSchemaZ, } from "./tmux-server-owners.js";
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
const FileSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
version: z.literal(1),
|
|
14
|
+
servers: z.array(TmuxServerRegistrationSchemaZ).max(MAX_TMUX_SERVER_OWNERS),
|
|
15
|
+
})
|
|
16
|
+
.strict()
|
|
17
|
+
.superRefine((file, ctx) => {
|
|
18
|
+
if (new Set(file.servers.map((server) => server.serverId)).size !== file.servers.length)
|
|
19
|
+
ctx.addIssue({ code: "custom", message: "Duplicate server registration" });
|
|
20
|
+
});
|
|
21
|
+
export function readTmuxServerRegistrations(path) {
|
|
22
|
+
try {
|
|
23
|
+
return FileSchema.parse(JSON.parse(readFileSync(path, "utf8"))).servers;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === "ENOENT")
|
|
27
|
+
return [];
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeTmuxServerRegistrations(path, registrations) {
|
|
32
|
+
const file = FileSchema.parse({ version: 1, servers: registrations });
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
34
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(temporary, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
37
|
+
renameSync(temporary, path);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
try {
|
|
41
|
+
unlinkSync(temporary);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* rename already removed it */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Discovery never creates a server, loads configuration, or attaches a client. */
|
|
49
|
+
export function createTmuxServerProbe(executable) {
|
|
50
|
+
const executablePath = realpathSync(executable);
|
|
51
|
+
const env = tmuxClientEnvironment(process.env);
|
|
52
|
+
return async (selector) => {
|
|
53
|
+
const args = selector.kind === "path" ? ["-S", selector.path] : ["-L", selector.name];
|
|
54
|
+
const read = async (selection) => (await exec(executablePath, [...selection, "-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"], { env, encoding: "utf8", timeout: 500, maxBuffer: 8192 })).stdout.trimEnd();
|
|
55
|
+
try {
|
|
56
|
+
const first = await read(args);
|
|
57
|
+
const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(first);
|
|
58
|
+
if (!match)
|
|
59
|
+
return null;
|
|
60
|
+
const socket = captureUnixSocketIdentity(match[1]);
|
|
61
|
+
if ((await read(["-S", socket.path])) !== first)
|
|
62
|
+
return null;
|
|
63
|
+
revalidateUnixSocketIdentity(socket);
|
|
64
|
+
return {
|
|
65
|
+
nativeServerIdentity: { pid: match[2], startTime: match[3] },
|
|
66
|
+
fingerprint: createHash("sha256")
|
|
67
|
+
.update(JSON.stringify([socket.path, match[2], match[3]]))
|
|
68
|
+
.digest("hex"),
|
|
69
|
+
authority: { executablePath, socketSelector: { kind: "path", path: socket.path } },
|
|
70
|
+
valid: () => {
|
|
71
|
+
try {
|
|
72
|
+
revalidateUnixSocketIdentity(socket);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { WorkspaceSessionCreateArgumentsSchemaZ, } from "@tmux-ide/contracts";
|
|
2
|
+
import { FleetLifecycleAuthority } from "./fleet-lifecycle-authority.js";
|
|
3
|
+
/** Bounded online-server creation. The existing lifecycle owns idempotency and workspace naming. */
|
|
4
|
+
export function createNativeTmuxSessionCreator(options) {
|
|
5
|
+
let disposed = false;
|
|
6
|
+
const pending = new Set();
|
|
7
|
+
const assertOpen = () => {
|
|
8
|
+
options.assertOpen();
|
|
9
|
+
if (disposed)
|
|
10
|
+
throw new Error("Session creation is retired");
|
|
11
|
+
};
|
|
12
|
+
const authority = new FleetLifecycleAuthority({
|
|
13
|
+
daemonInstanceId: options.generation,
|
|
14
|
+
productVersion: "scoped-owner",
|
|
15
|
+
startedAt: new Date().toISOString(),
|
|
16
|
+
registry: options.registry,
|
|
17
|
+
runTmux(args) {
|
|
18
|
+
assertOpen();
|
|
19
|
+
return options.run(args);
|
|
20
|
+
},
|
|
21
|
+
readFleet: () => [],
|
|
22
|
+
ensureChromeUpdater: false,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
createSession(operationId, input) {
|
|
26
|
+
assertOpen();
|
|
27
|
+
const parsed = WorkspaceSessionCreateArgumentsSchemaZ.parse(input);
|
|
28
|
+
if (parsed.expectedDaemonInstanceId && parsed.expectedDaemonInstanceId !== options.generation)
|
|
29
|
+
throw new Error("Selected tmux server generation changed");
|
|
30
|
+
if (pending.size >= 16)
|
|
31
|
+
throw new Error("Session creation capacity reached");
|
|
32
|
+
const operation = authority
|
|
33
|
+
.createSession(operationId, options.generation, parsed)
|
|
34
|
+
.finally(() => pending.delete(operation));
|
|
35
|
+
pending.add(operation);
|
|
36
|
+
return operation;
|
|
37
|
+
},
|
|
38
|
+
async dispose() {
|
|
39
|
+
disposed = true;
|
|
40
|
+
await Promise.allSettled([...pending]);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|