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
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
type WorkspaceMultiplexerWindowTarget,
|
|
29
29
|
type SessionRuntimePaneReadIntent,
|
|
30
30
|
type Workspace,
|
|
31
|
+
type WindowLinkTarget,
|
|
32
|
+
type WindowLinkTopology,
|
|
31
33
|
} from "@tmux-ide/contracts";
|
|
32
34
|
import { TmuxError } from "@tmux-ide/tmux-bridge";
|
|
33
35
|
|
|
@@ -46,6 +48,8 @@ import {
|
|
|
46
48
|
} from "./tmux-interaction-options.ts";
|
|
47
49
|
import { memorablePaneName } from "../terminal/protocol/pane-display-name.ts";
|
|
48
50
|
|
|
51
|
+
import { WindowLinkResolutionError } from "../terminal/mirror/window-link-authority.ts";
|
|
52
|
+
|
|
49
53
|
const CREATION_OPTION = "@tmux_ide_creation_id";
|
|
50
54
|
const SEMANTIC_PANE_OPTION = "@tmux_ide_pane_id";
|
|
51
55
|
const SEMANTIC_WINDOW_OPTION = "@tmux_ide_window_id";
|
|
@@ -68,6 +72,10 @@ export type WorkspaceMultiplexerErrorCode =
|
|
|
68
72
|
| "pane_not_found"
|
|
69
73
|
| "window_not_found"
|
|
70
74
|
| "ambiguous_target"
|
|
75
|
+
| "window_link_stale"
|
|
76
|
+
| "window_link_ambiguous"
|
|
77
|
+
| "window_link_session_mismatch"
|
|
78
|
+
| "window_link_backing_mismatch"
|
|
71
79
|
/** Refused: killing it would take the whole session with it. */
|
|
72
80
|
| "last_window_refused"
|
|
73
81
|
/** Refused: killing it would take the whole session with it. */
|
|
@@ -88,6 +96,10 @@ const ERROR_MESSAGES: Readonly<Record<WorkspaceMultiplexerErrorCode, string>> =
|
|
|
88
96
|
workspace_unavailable: "The requested workspace is not available for multiplexer verbs.",
|
|
89
97
|
pane_not_found: "No pane in this workspace carries the requested semantic identity.",
|
|
90
98
|
window_not_found: "No window in this workspace carries the requested identity.",
|
|
99
|
+
window_link_stale: "The window link observation is no longer current.",
|
|
100
|
+
window_link_ambiguous: "Select an explicit window link for this pane.",
|
|
101
|
+
window_link_session_mismatch: "The window link belongs to another session generation.",
|
|
102
|
+
window_link_backing_mismatch: "The window link no longer names the expected window.",
|
|
91
103
|
ambiguous_target: "The requested identity names more than one live tmux object.",
|
|
92
104
|
last_window_refused:
|
|
93
105
|
"This is the session's last window. Close the session instead if that is what you mean.",
|
|
@@ -201,7 +213,19 @@ export function parseMultiplexerPaneRows(output: string): readonly MultiplexerPa
|
|
|
201
213
|
creationId: creationId === "" ? null : creationId,
|
|
202
214
|
});
|
|
203
215
|
}
|
|
204
|
-
|
|
216
|
+
// list-panes -s repeats each backing for its session links. Identical native
|
|
217
|
+
// rows are one pane; conflicting observations fail closed.
|
|
218
|
+
const unique = new Map<string, MultiplexerPaneRow>();
|
|
219
|
+
for (const row of rows) {
|
|
220
|
+
const previous = unique.get(row.paneId);
|
|
221
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(row)) {
|
|
222
|
+
throw new WorkspaceMultiplexerError("workspace_unavailable", {
|
|
223
|
+
reason: "conflicting_linked_pane_observation",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
unique.set(row.paneId, row);
|
|
227
|
+
}
|
|
228
|
+
return [...unique.values()];
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
/** Distinct windows in a pane listing, in first-seen order. */
|
|
@@ -288,6 +312,17 @@ const DEFAULT_IO: Omit<WorkspaceMultiplexerIo, "runTmux"> = {
|
|
|
288
312
|
|
|
289
313
|
const MAX_CACHED_SESSIONS = 512;
|
|
290
314
|
|
|
315
|
+
export interface AuthoritativeWindowLinkAction {
|
|
316
|
+
readonly action: "select" | "unlink";
|
|
317
|
+
readonly target?: WindowLinkTarget;
|
|
318
|
+
/** Semantic pane identity, resolved by the retained mirror authority. */
|
|
319
|
+
readonly paneId?: string;
|
|
320
|
+
}
|
|
321
|
+
export interface AuthoritativeWindowLinkActionResult {
|
|
322
|
+
readonly outcome: "applied" | "stale" | "native-refused" | "indeterminate";
|
|
323
|
+
readonly windowLinks: WindowLinkTopology | null;
|
|
324
|
+
}
|
|
325
|
+
|
|
291
326
|
export class WorkspaceMultiplexerAuthority {
|
|
292
327
|
readonly #daemonInstanceId: string;
|
|
293
328
|
readonly #registry: WorkspaceRegistry;
|
|
@@ -329,6 +364,82 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
329
364
|
return this.#mutate(raw, timing);
|
|
330
365
|
}
|
|
331
366
|
|
|
367
|
+
/** Runs only inside the existing semantic mutation lane; never rediscovers a stale link. */
|
|
368
|
+
async mutateWindowLink(
|
|
369
|
+
raw: WorkspaceMultiplexerMutationRequest,
|
|
370
|
+
execute: (
|
|
371
|
+
session: string,
|
|
372
|
+
action: AuthoritativeWindowLinkAction,
|
|
373
|
+
) => Promise<AuthoritativeWindowLinkActionResult>,
|
|
374
|
+
): Promise<WorkspaceMultiplexerMutationResult> {
|
|
375
|
+
if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
|
|
376
|
+
const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
|
|
377
|
+
if (request.expectedDaemonInstanceId !== this.#daemonInstanceId)
|
|
378
|
+
throw new WorkspaceMultiplexerError("daemon_instance_mismatch");
|
|
379
|
+
const intent = request.intent;
|
|
380
|
+
if (
|
|
381
|
+
intent.verb !== "workspace.window.link.select" &&
|
|
382
|
+
intent.verb !== "workspace.window.link.unlink" &&
|
|
383
|
+
intent.verb !== "workspace.pane.select"
|
|
384
|
+
)
|
|
385
|
+
throw new TypeError("Expected a window-link selection or unlink intent");
|
|
386
|
+
const workspace = this.#registry.get(intent.workspaceName);
|
|
387
|
+
if (!workspace) throw new WorkspaceMultiplexerError("workspace_not_found");
|
|
388
|
+
const envelope = {
|
|
389
|
+
operationId: request.operationId,
|
|
390
|
+
daemonInstanceId: this.#daemonInstanceId,
|
|
391
|
+
workspaceName: intent.workspaceName,
|
|
392
|
+
};
|
|
393
|
+
try {
|
|
394
|
+
const result = await execute(
|
|
395
|
+
workspace.sessionName,
|
|
396
|
+
intent.verb === "workspace.pane.select"
|
|
397
|
+
? {
|
|
398
|
+
action: "select",
|
|
399
|
+
...(intent.windowLink ? { target: intent.windowLink } : {}),
|
|
400
|
+
paneId: intent.semanticPaneId,
|
|
401
|
+
}
|
|
402
|
+
: {
|
|
403
|
+
action: intent.verb === "workspace.window.link.unlink" ? "unlink" : "select",
|
|
404
|
+
target: intent.target,
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
if (result.outcome !== "applied") {
|
|
408
|
+
throw new WorkspaceMultiplexerError(
|
|
409
|
+
result.outcome === "stale"
|
|
410
|
+
? "window_link_stale"
|
|
411
|
+
: result.outcome === "native-refused"
|
|
412
|
+
? "mutation_failed"
|
|
413
|
+
: "mutation_unverified",
|
|
414
|
+
{ reason: result.outcome },
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return WorkspaceMultiplexerMutationResultSchemaZ.parse(
|
|
418
|
+
intent.verb === "workspace.pane.select"
|
|
419
|
+
? {
|
|
420
|
+
...envelope,
|
|
421
|
+
verb: intent.verb,
|
|
422
|
+
outcome: "applied",
|
|
423
|
+
semanticPaneId: intent.semanticPaneId,
|
|
424
|
+
}
|
|
425
|
+
: { ...envelope, verb: intent.verb, outcome: "applied", target: intent.target },
|
|
426
|
+
);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error instanceof WorkspaceMultiplexerError) throw error;
|
|
429
|
+
if (error instanceof WindowLinkResolutionError)
|
|
430
|
+
throw new WorkspaceMultiplexerError(
|
|
431
|
+
error.reason,
|
|
432
|
+
{ operationId: request.operationId },
|
|
433
|
+
error,
|
|
434
|
+
);
|
|
435
|
+
throw new WorkspaceMultiplexerError(
|
|
436
|
+
"mutation_unverified",
|
|
437
|
+
{ operationId: request.operationId },
|
|
438
|
+
error,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
332
443
|
/**
|
|
333
444
|
* Capture a semantically addressed pane for an authored read. The tmux
|
|
334
445
|
* after-capture hook is the completion authority; this method only performs
|
|
@@ -608,6 +719,11 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
608
719
|
} as const;
|
|
609
720
|
|
|
610
721
|
switch (intent.verb) {
|
|
722
|
+
case "workspace.window.link.select":
|
|
723
|
+
case "workspace.window.link.unlink":
|
|
724
|
+
throw new WorkspaceMultiplexerError("workspace_unavailable", {
|
|
725
|
+
reason: "window_link_authority_required",
|
|
726
|
+
});
|
|
611
727
|
case "workspace.window.split":
|
|
612
728
|
return this.#split(request, workspace, envelope);
|
|
613
729
|
case "workspace.window.kill":
|
|
@@ -1039,6 +1155,10 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
1039
1155
|
envelope: { operationId: string; daemonInstanceId: string; workspaceName: string },
|
|
1040
1156
|
timing?: WorkspaceMultiplexerOperationTiming,
|
|
1041
1157
|
): WorkspaceMultiplexerMutationResult {
|
|
1158
|
+
if (intent.windowLink)
|
|
1159
|
+
throw new WorkspaceMultiplexerError("workspace_unavailable", {
|
|
1160
|
+
reason: "window_link_authority_required",
|
|
1161
|
+
});
|
|
1042
1162
|
const timed = <T>(operation: string, run: () => T): T => {
|
|
1043
1163
|
if (!timing) return run();
|
|
1044
1164
|
let startedAtMicros: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { boundedTmuxRead } from "./bounded-tmux-read.ts";
|
|
2
2
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
WorkspacePaneCreateMutationRequestSchemaZ,
|
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
} from "@tmux-ide/contracts";
|
|
14
14
|
import { runTmuxBinary, TmuxError } from "@tmux-ide/tmux-bridge";
|
|
15
15
|
import { createNamedSocketFence } from "./tmux-named-socket-fence.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
resolveTmuxExecutable as resolvePinnedTmuxExecutable,
|
|
18
|
+
tmuxClientEnvironment,
|
|
19
|
+
} from "./tmux-client-execution.ts";
|
|
17
20
|
|
|
18
21
|
import { probeProjectReadiness } from "./project-readiness-probe.ts";
|
|
19
22
|
import { agentHintForCommand } from "./agent-kind.ts";
|
|
@@ -26,7 +29,7 @@ import { getDefaultWorkspaceRegistry, type WorkspaceRegistry } from "./workspace
|
|
|
26
29
|
import { memorablePaneName } from "../terminal/protocol/pane-display-name.ts";
|
|
27
30
|
import { shellEscape } from "./shell.ts";
|
|
28
31
|
import { MissionRepository } from "./mission-repository.ts";
|
|
29
|
-
import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
|
|
32
|
+
import { resolveRuntimeNamespace, resolveTmuxServerIntent } from "./runtime-namespace.ts";
|
|
30
33
|
import { prepareTmuxTruecolorEnvironment } from "./tmux-terminal-color.ts";
|
|
31
34
|
import {
|
|
32
35
|
captureUnixSocketIdentity,
|
|
@@ -220,57 +223,15 @@ function assertEffectiveConfigProvenance(
|
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
function resolveTmuxExecutable(): string {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
: (process.env.PATH ?? "")
|
|
232
|
-
.split(delimiter)
|
|
233
|
-
// Empty and relative PATH entries mean the daemon/project cwd. They
|
|
234
|
-
// are never executable authority for a privileged tmux mutation.
|
|
235
|
-
.filter((entry) => entry.length > 0 && isAbsolute(entry))
|
|
236
|
-
.map((entry) => join(entry, "tmux"));
|
|
237
|
-
for (const candidate of candidates) {
|
|
238
|
-
try {
|
|
239
|
-
if (!isAbsolute(candidate)) continue;
|
|
240
|
-
accessSync(candidate, constants.X_OK);
|
|
241
|
-
const canonical = realpathSync(candidate);
|
|
242
|
-
if (statSync(canonical).isFile()) return canonical;
|
|
243
|
-
} catch {
|
|
244
|
-
// Continue to the next daemon-start candidate.
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
throw new WorkspacePaneCreationError("workspace_unavailable", {
|
|
248
|
-
reason: "tmux_executable_unavailable",
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const SAFE_TERMINAL_VALUE = /^(?:xterm|screen|tmux|rxvt|vt100|ansi)[A-Za-z0-9+._-]{0,58}$/u;
|
|
253
|
-
const SAFE_LOCALE_VALUE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}$/u;
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* The pinned tmux client receives only presentation metadata. Everything else
|
|
257
|
-
* is denied by omission: PATH/HOME/SHELL, TMUX/TMUX_PANE/TMUX_TMPDIR, shell
|
|
258
|
-
* startup hooks, dynamic-loader variables, Node options, and project secrets
|
|
259
|
-
* cannot redirect authority or inject code into this daemon-owned execution.
|
|
260
|
-
*/
|
|
261
|
-
function tmuxClientEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
262
|
-
const environment: NodeJS.ProcessEnv = {
|
|
263
|
-
TERM: SAFE_TERMINAL_VALUE.test(source.TERM ?? "") ? source.TERM : "xterm-256color",
|
|
264
|
-
// A pinned runner may be the first tmux client and therefore create the
|
|
265
|
-
// server. Never let a headless parent (`TERM=dumb`, `NO_COLOR=1`) become
|
|
266
|
-
// the global environment inherited by every later interactive child.
|
|
267
|
-
COLORTERM: "truecolor",
|
|
268
|
-
};
|
|
269
|
-
for (const name of ["LANG", "LC_ALL", "LC_CTYPE"] as const) {
|
|
270
|
-
const value = source[name];
|
|
271
|
-
if (value && SAFE_LOCALE_VALUE.test(value)) environment[name] = value;
|
|
226
|
+
// Preserve development artifact qualification errors instead of masking them.
|
|
227
|
+
if (resolveRuntimeNamespace().development) return resolvePinnedTmuxExecutable();
|
|
228
|
+
try {
|
|
229
|
+
return resolvePinnedTmuxExecutable();
|
|
230
|
+
} catch {
|
|
231
|
+
throw new WorkspacePaneCreationError("workspace_unavailable", {
|
|
232
|
+
reason: "tmux_executable_unavailable",
|
|
233
|
+
});
|
|
272
234
|
}
|
|
273
|
-
return environment;
|
|
274
235
|
}
|
|
275
236
|
|
|
276
237
|
function tmuxSocketFromEnvironment(): string | null {
|
|
@@ -281,7 +242,7 @@ function tmuxSocketFromEnvironment(): string | null {
|
|
|
281
242
|
export function resolveWorkspacePaneTmuxAuthority(): WorkspacePaneTmuxAuthority {
|
|
282
243
|
const namespace = resolveRuntimeNamespace();
|
|
283
244
|
const executablePath = resolveTmuxExecutable();
|
|
284
|
-
if (
|
|
245
|
+
if (resolveTmuxServerIntent()?.source === "explicit")
|
|
285
246
|
return Object.freeze({ executablePath, socketSelector: namespace.tmuxSocket });
|
|
286
247
|
const environmentSocket = tmuxSocketFromEnvironment();
|
|
287
248
|
if (environmentSocket) {
|
|
@@ -305,7 +266,7 @@ export function resolveWorkspacePaneTmuxAuthority(): WorkspacePaneTmuxAuthority
|
|
|
305
266
|
// A sessionless daemon may start before the default tmux server exists. Pin
|
|
306
267
|
// the daemon's default socket *name* and captured environment rather than
|
|
307
268
|
// consulting a later request's TMUX/PATH.
|
|
308
|
-
return Object.freeze({ executablePath, socketSelector:
|
|
269
|
+
return Object.freeze({ executablePath, socketSelector: namespace.tmuxSocket });
|
|
309
270
|
}
|
|
310
271
|
|
|
311
272
|
/** Execute mutations only through the daemon-generation-pinned tmux authority. */
|
|
@@ -122,6 +122,7 @@ const PANE_VERIFY_FORMAT = [
|
|
|
122
122
|
"#{pane_id}",
|
|
123
123
|
`#{${SEMANTIC_PANE_OPTION}}`,
|
|
124
124
|
`#{${SEMANTIC_WINDOW_OPTION}}`,
|
|
125
|
+
"#{window_index}",
|
|
125
126
|
SENTINEL,
|
|
126
127
|
].join(FIELD);
|
|
127
128
|
|
|
@@ -209,6 +210,7 @@ interface ScanPane {
|
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
interface VerifyPane {
|
|
213
|
+
readonly windowIndex: number;
|
|
212
214
|
readonly sessionId: string;
|
|
213
215
|
readonly windowId: string;
|
|
214
216
|
readonly windowName: string;
|
|
@@ -384,8 +386,10 @@ function parseVerifyPanes(output: string): VerifyPane[] {
|
|
|
384
386
|
const windowPaneCount = positiveInteger(fields[3] ?? "");
|
|
385
387
|
const sessionWindowCount = positiveInteger(fields[4] ?? "");
|
|
386
388
|
if (
|
|
387
|
-
fields.length !==
|
|
388
|
-
fields[
|
|
389
|
+
fields.length !== 10 ||
|
|
390
|
+
fields[9] !== SENTINEL ||
|
|
391
|
+
!/^(0|[1-9][0-9]*)$/u.test(fields[8]!) ||
|
|
392
|
+
!Number.isSafeInteger(Number(fields[8])) ||
|
|
389
393
|
!/^\$[0-9]+$/u.test(fields[0]!) ||
|
|
390
394
|
!/^@[0-9]+$/u.test(fields[1]!) ||
|
|
391
395
|
!/^%[0-9]+$/u.test(fields[5]!) ||
|
|
@@ -405,6 +409,7 @@ function parseVerifyPanes(output: string): VerifyPane[] {
|
|
|
405
409
|
paneId: fields[5]!,
|
|
406
410
|
semanticPaneId: fields[6]!,
|
|
407
411
|
semanticWindowId: fields[7]!,
|
|
412
|
+
windowIndex: Number(fields[8]),
|
|
408
413
|
});
|
|
409
414
|
}
|
|
410
415
|
return panes;
|
|
@@ -565,9 +570,7 @@ export class WorkspacePromotionAuthority {
|
|
|
565
570
|
// promotion is the app's only repair verb — so reconcile the stamps here
|
|
566
571
|
// before resolving to a `replayed` outcome. Stamping is additive and never
|
|
567
572
|
// overwrites a valid stamp, so a healthy session is untouched.
|
|
568
|
-
const alreadyRegistered = this.#
|
|
569
|
-
.list()
|
|
570
|
-
.find((workspace) => workspace.sessionName === session.sessionName);
|
|
573
|
+
const alreadyRegistered = this.#registeredSession(session.sessionName);
|
|
571
574
|
if (alreadyRegistered) {
|
|
572
575
|
const registeredIdentity: PromotionIdentity = {
|
|
573
576
|
workspaceName: alreadyRegistered.name,
|
|
@@ -590,6 +593,42 @@ export class WorkspacePromotionAuthority {
|
|
|
590
593
|
await this.#verifyPromotedInventory(session.sessionId, identity);
|
|
591
594
|
await this.#publishFleetEnrollment(request, session, identity);
|
|
592
595
|
|
|
596
|
+
// Catalog discovery can admit this session while the native stamping
|
|
597
|
+
// and verification awaits run. Reuse that exact registry identity instead
|
|
598
|
+
// of publishing a second name for the same session at the commit point.
|
|
599
|
+
const discovered = this.#registeredSession(session.sessionName);
|
|
600
|
+
if (discovered) {
|
|
601
|
+
if (discovered.name !== identity.workspaceName) {
|
|
602
|
+
try {
|
|
603
|
+
await this.#io.runTmux([
|
|
604
|
+
"set-option",
|
|
605
|
+
"-t",
|
|
606
|
+
session.sessionId,
|
|
607
|
+
SESSION_WORKSPACE_OPTION,
|
|
608
|
+
discovered.name,
|
|
609
|
+
]);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
throw new WorkspacePromotionError(
|
|
612
|
+
"stamp_failed",
|
|
613
|
+
{
|
|
614
|
+
operationId: request.operationId,
|
|
615
|
+
workspaceName: discovered.name,
|
|
616
|
+
},
|
|
617
|
+
error,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
this.#assertActive(request.operationId);
|
|
622
|
+
if (this.#registeredSession(session.sessionName)?.name !== discovered.name) {
|
|
623
|
+
throw new WorkspacePromotionError("workspace_conflict", {
|
|
624
|
+
operationId: request.operationId,
|
|
625
|
+
workspaceName: discovered.name,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return this.#succeed(request, fingerprint, discovered.name, session.sessionName, {
|
|
629
|
+
replayed: true,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
593
632
|
let registered: Workspace;
|
|
594
633
|
try {
|
|
595
634
|
registered = this.#registry.add({
|
|
@@ -651,6 +690,16 @@ export class WorkspacePromotionAuthority {
|
|
|
651
690
|
}
|
|
652
691
|
}
|
|
653
692
|
|
|
693
|
+
#registeredSession(sessionName: string): Workspace | undefined {
|
|
694
|
+
const candidates = this.#registry
|
|
695
|
+
.list()
|
|
696
|
+
.filter((workspace) => workspace.sessionName === sessionName);
|
|
697
|
+
if (candidates.length > 1) {
|
|
698
|
+
throw new WorkspacePromotionError("workspace_conflict", { sessionName });
|
|
699
|
+
}
|
|
700
|
+
return candidates[0];
|
|
701
|
+
}
|
|
702
|
+
|
|
654
703
|
#assertConflictFreeIdentity(identity: PromotionIdentity): void {
|
|
655
704
|
for (const workspace of this.#registry.list()) {
|
|
656
705
|
if (
|
|
@@ -757,6 +806,19 @@ export class WorkspacePromotionAuthority {
|
|
|
757
806
|
});
|
|
758
807
|
}
|
|
759
808
|
|
|
809
|
+
// list-panes -s repeats each backing pane once per window link. Stamp the
|
|
810
|
+
// backing only once, and reject contradictory observations before writes.
|
|
811
|
+
const uniqueScan = new Map<string, ScanPane>();
|
|
812
|
+
for (const pane of scanned) {
|
|
813
|
+
const previous = uniqueScan.get(pane.paneId);
|
|
814
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(pane)) {
|
|
815
|
+
throw new WorkspacePromotionError("promotion_verification_failed", {
|
|
816
|
+
reason: "inconsistent_tmux_topology",
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
uniqueScan.set(pane.paneId, pane);
|
|
820
|
+
}
|
|
821
|
+
scanned = [...uniqueScan.values()];
|
|
760
822
|
const nowSec = Math.floor(this.#io.now() / 1000);
|
|
761
823
|
const reconciledWindows = new Set<string>();
|
|
762
824
|
try {
|
|
@@ -928,8 +990,32 @@ export class WorkspacePromotionAuthority {
|
|
|
928
990
|
});
|
|
929
991
|
}
|
|
930
992
|
|
|
993
|
+
// Validate links independently from their shared backing. Repeated native
|
|
994
|
+
// panes are legal only across distinct links with identical backing facts.
|
|
995
|
+
const links = new Map<number, VerifyPane[]>();
|
|
996
|
+
const backingPanes = new Map<string, Omit<VerifyPane, "windowIndex">>();
|
|
997
|
+
const semanticWindows = new Map<string, string>();
|
|
998
|
+
for (const pane of panes) {
|
|
999
|
+
const { windowIndex, ...backing } = pane;
|
|
1000
|
+
const previous = backingPanes.get(pane.paneId);
|
|
1001
|
+
const semanticOwner = semanticWindows.get(pane.semanticWindowId);
|
|
1002
|
+
const rows = links.get(windowIndex) ?? [];
|
|
1003
|
+
if (
|
|
1004
|
+
(previous && JSON.stringify(previous) !== JSON.stringify(backing)) ||
|
|
1005
|
+
(semanticOwner && semanticOwner !== pane.windowId) ||
|
|
1006
|
+
rows.some((row) => row.paneId === pane.paneId || row.windowId !== pane.windowId)
|
|
1007
|
+
) {
|
|
1008
|
+
throw new WorkspacePromotionError("promotion_verification_failed", {
|
|
1009
|
+
reason: "inconsistent_tmux_topology",
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
backingPanes.set(pane.paneId, backing);
|
|
1013
|
+
semanticWindows.set(pane.semanticWindowId, pane.windowId);
|
|
1014
|
+
rows.push(pane);
|
|
1015
|
+
links.set(windowIndex, rows);
|
|
1016
|
+
}
|
|
931
1017
|
const catalog = analyzeTrustedSemanticPaneCatalog(
|
|
932
|
-
|
|
1018
|
+
[...backingPanes.values()].map((pane) => ({
|
|
933
1019
|
workspaceName: identity.workspaceName,
|
|
934
1020
|
semanticPaneId: pane.semanticPaneId === "" ? null : pane.semanticPaneId,
|
|
935
1021
|
sessionId: pane.sessionId,
|
|
@@ -950,17 +1036,17 @@ export class WorkspacePromotionAuthority {
|
|
|
950
1036
|
});
|
|
951
1037
|
}
|
|
952
1038
|
|
|
953
|
-
const
|
|
954
|
-
for (const pane of
|
|
955
|
-
const rows =
|
|
1039
|
+
const backings = new Map<string, Omit<VerifyPane, "windowIndex">[]>();
|
|
1040
|
+
for (const pane of backingPanes.values()) {
|
|
1041
|
+
const rows = backings.get(pane.windowId) ?? [];
|
|
956
1042
|
rows.push(pane);
|
|
957
|
-
|
|
1043
|
+
backings.set(pane.windowId, rows);
|
|
958
1044
|
}
|
|
959
1045
|
const sessionWindowCount = panes[0]!.sessionWindowCount;
|
|
960
1046
|
if (
|
|
961
|
-
|
|
1047
|
+
links.size !== sessionWindowCount ||
|
|
962
1048
|
panes.some((pane) => pane.sessionWindowCount !== sessionWindowCount) ||
|
|
963
|
-
[...
|
|
1049
|
+
[...links.values(), ...backings.values()].some(
|
|
964
1050
|
(rows) =>
|
|
965
1051
|
rows.length !== rows[0]!.windowPaneCount ||
|
|
966
1052
|
rows.some(
|
|
@@ -173,6 +173,11 @@ export class WorkspaceRegistry {
|
|
|
173
173
|
return this.workspaces.some((w) => w.name === name);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/** Whether this entry is discovered live state, never durable user intent. */
|
|
177
|
+
isVolatile(name: string): boolean {
|
|
178
|
+
return this.volatileNames.has(name);
|
|
179
|
+
}
|
|
180
|
+
|
|
176
181
|
add(input: AddWorkspaceInput): Workspace {
|
|
177
182
|
if (this.has(input.name)) {
|
|
178
183
|
throw new WorkspaceAlreadyExistsError(input.name);
|
|
@@ -33,9 +33,11 @@ function rejectUpgrade(socket: Socket, status: number): void {
|
|
|
33
33
|
? "Forbidden"
|
|
34
34
|
: status === 404
|
|
35
35
|
? "Not Found"
|
|
36
|
-
: status ===
|
|
37
|
-
? "
|
|
38
|
-
:
|
|
36
|
+
: status === 410
|
|
37
|
+
? "Gone"
|
|
38
|
+
: status === 426
|
|
39
|
+
? "Upgrade Required"
|
|
40
|
+
: "Service Unavailable";
|
|
39
41
|
try {
|
|
40
42
|
socket.end(`HTTP/1.1 ${status} ${phrase}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
41
43
|
} catch {
|
|
@@ -56,6 +58,8 @@ export interface PaneStreamWebSocketBoundary {
|
|
|
56
58
|
export function attachPaneStreamWebSocket(
|
|
57
59
|
server: Server,
|
|
58
60
|
coordinator: PaneStreamAdmissionCoordinator,
|
|
61
|
+
redemptionPath: string = PANE_STREAM_REDEEM_PATH,
|
|
62
|
+
isCurrent: () => boolean = () => true,
|
|
59
63
|
): PaneStreamWebSocketBoundary {
|
|
60
64
|
const wss = new WebSocketServer({
|
|
61
65
|
noServer: true,
|
|
@@ -77,8 +81,20 @@ export function attachPaneStreamWebSocket(
|
|
|
77
81
|
): void => {
|
|
78
82
|
const rawPath = request.url ?? "";
|
|
79
83
|
const pathname = rawPath.split("?", 1)[0] ?? "";
|
|
80
|
-
if (
|
|
81
|
-
|
|
84
|
+
if (
|
|
85
|
+
redemptionPath === PANE_STREAM_REDEEM_PATH &&
|
|
86
|
+
pathname.startsWith("/v1/terminal/pane-streams/")
|
|
87
|
+
) {
|
|
88
|
+
rejectUpgrade(socket, 426);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (redemptionPath !== PANE_STREAM_REDEEM_PATH && pathname !== redemptionPath) return;
|
|
92
|
+
if (
|
|
93
|
+
redemptionPath === PANE_STREAM_REDEEM_PATH &&
|
|
94
|
+
!pathname.startsWith("/v2/terminal/pane-streams/")
|
|
95
|
+
)
|
|
96
|
+
return;
|
|
97
|
+
if (pathname !== redemptionPath) {
|
|
82
98
|
rejectUpgrade(socket, 404);
|
|
83
99
|
return;
|
|
84
100
|
}
|
|
@@ -102,6 +118,10 @@ export function attachPaneStreamWebSocket(
|
|
|
102
118
|
rejectUpgrade(socket, 403);
|
|
103
119
|
return;
|
|
104
120
|
}
|
|
121
|
+
if (!isCurrent()) {
|
|
122
|
+
rejectUpgrade(socket, 410);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
105
125
|
const decision = coordinator.reserveUpgrade({
|
|
106
126
|
path: rawPath,
|
|
107
127
|
protocols: protocols(protocolHeaders[0]),
|