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.
- package/bin/cli.js +52253 -49070
- package/package.json +1 -1
- 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/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 +345 -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 +16 -10
- 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-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-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- 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-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/fleet-lifecycle-client.js +42 -1
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- 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 +468 -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 +16 -8
- 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-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-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -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-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -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/fleet-lifecycle-client.ts +52 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mountTmuxServerRoutes } from "./tmux-servers.js";
|
|
1
2
|
import { streamBoundedLogs } from "./log-stream.js";
|
|
2
3
|
import { mountWorkspaceAdmissionRoute } from "./resources/workspace-admission-route.js";
|
|
3
4
|
import { mountFleetPreviewRoute } from "./resources/fleet-preview-route.js";
|
|
@@ -317,6 +318,11 @@ export function createApp(options = {}) {
|
|
|
317
318
|
const app = new Hono();
|
|
318
319
|
// Allow cross-origin (Next.js dashboard, Tailscale, etc.)
|
|
319
320
|
app.use("/*", cors());
|
|
321
|
+
if (options.tmuxServerOwners)
|
|
322
|
+
mountTmuxServerRoutes(app, {
|
|
323
|
+
ownerToken: options.remoteAccess?.ownerToken ?? null,
|
|
324
|
+
owners: options.tmuxServerOwners,
|
|
325
|
+
});
|
|
320
326
|
// Owner-only reads use the same early routing boundary as issuance below:
|
|
321
327
|
// remote/project credentials neither authorize nor block the owner bearer.
|
|
322
328
|
mountDiagnosticsRoute(app, {
|
|
@@ -506,10 +512,24 @@ export function createApp(options = {}) {
|
|
|
506
512
|
// Credential-free endpoint binding. A desktop host reads the nonce from the
|
|
507
513
|
// owner-only canonical record, probes this endpoint, and compares before it
|
|
508
514
|
// sends any remote-access or local-bypass credential.
|
|
509
|
-
|
|
515
|
+
// Coalesce concurrent probes, but never reuse a completed server proof.
|
|
516
|
+
let pendingTmuxProof = null;
|
|
517
|
+
app.get("/identity", async (c) => {
|
|
518
|
+
const wantsTmuxProof = c.req.query("tmuxServerProof") === "1";
|
|
519
|
+
let tmuxServerProof = null;
|
|
520
|
+
if (wantsTmuxProof && options.tmuxServerProof) {
|
|
521
|
+
pendingTmuxProof ??= Promise.resolve()
|
|
522
|
+
.then(options.tmuxServerProof)
|
|
523
|
+
.catch(() => null)
|
|
524
|
+
.finally(() => {
|
|
525
|
+
pendingTmuxProof = null;
|
|
526
|
+
});
|
|
527
|
+
tmuxServerProof = await pendingTmuxProof;
|
|
528
|
+
}
|
|
510
529
|
return c.json({
|
|
511
530
|
ok: true,
|
|
512
531
|
pid: process.pid,
|
|
532
|
+
...(wantsTmuxProof && options.tmuxServerProof ? { tmuxServerProof } : {}),
|
|
513
533
|
protocolVersion: DAEMON_WIRE_PROTOCOL_VERSION,
|
|
514
534
|
productVersion: daemonIdentity.productVersion,
|
|
515
535
|
instanceId: daemonIdentity.instanceId,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { TmuxServerScopeSchemaZ } from "@tmux-ide/contracts";
|
|
3
|
+
import { TmuxServerScopeError } from "../lib/tmux-server-owners.js";
|
|
4
|
+
import { mountTerminalNativeBackingRoute } from "./resources/terminal-native-backing-route.js";
|
|
5
|
+
/** Retain admission/coalescing limits for the lifetime of each independent owner. */
|
|
6
|
+
export function mountTmuxServerNativeBackingRoute(app, options) {
|
|
7
|
+
const routers = new WeakMap();
|
|
8
|
+
app.get(`${options.scopedPath}/native-backing/:workspaceName/:pane`, async (c) => {
|
|
9
|
+
const scope = TmuxServerScopeSchemaZ.safeParse({
|
|
10
|
+
serverId: c.req.param("serverId"),
|
|
11
|
+
generation: c.req.param("generation"),
|
|
12
|
+
});
|
|
13
|
+
if (!scope.success)
|
|
14
|
+
return c.json({ status: "invalid" }, 400);
|
|
15
|
+
try {
|
|
16
|
+
return await options.owners.withOwner(scope.data, async (owner) => {
|
|
17
|
+
let router = routers.get(owner);
|
|
18
|
+
if (!router) {
|
|
19
|
+
router = new Hono();
|
|
20
|
+
mountTerminalNativeBackingRoute(router, {
|
|
21
|
+
ownerToken: options.ownerToken,
|
|
22
|
+
generation: owner.generation,
|
|
23
|
+
resolveSession: (workspace) => owner.workspaceRegistry.get(workspace)?.sessionName ?? null,
|
|
24
|
+
capture: (session, pane, expected) => owner.sessionRuntimeRegistry.captureNativeBacking(session, pane, expected),
|
|
25
|
+
});
|
|
26
|
+
routers.set(owner, router);
|
|
27
|
+
}
|
|
28
|
+
const url = new URL(c.req.url);
|
|
29
|
+
url.pathname = `/api/project/${encodeURIComponent(c.req.param("workspaceName"))}/terminal-native-backing/${encodeURIComponent(c.req.param("pane"))}`;
|
|
30
|
+
return await router.request(new Request(url, c.req.raw));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return c.json({ status: error instanceof TmuxServerScopeError ? "retired" : "unavailable" }, error instanceof TmuxServerScopeError ? 409 : 503);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { mountTmuxServerNativeBackingRoute } from "./tmux-server-native-backing.js";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { streamSSE } from "hono/streaming";
|
|
4
|
+
import { projectApplicationShellResource } from "./resources/application-shell.js";
|
|
5
|
+
import { fleetSessionIdForName } from "./resources/fleet-catalog.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { TMUX_SERVERS_API_PATH, PANE_STREAM_ISSUE_PATH, TmuxServerIdSchemaZ, TmuxServerScopeSchemaZ, TmuxServersResourceSchemaZ, TmuxServerSessionsResourceSchemaZ, WorkspaceMultiplexerMutationRequestSchemaZ, WorkspacePaneCreateMutationRequestSchemaZ, WorkspaceSessionCreateArgumentsSchemaZ, } from "@tmux-ide/contracts";
|
|
8
|
+
import { TmuxServerRegistrationSchemaZ, TmuxServerScopeError, } from "../lib/tmux-server-owners.js";
|
|
9
|
+
import { WorkspaceMultiplexerError } from "../lib/workspace-multiplexer-verbs.js";
|
|
10
|
+
import { ownerBearerMatches } from "./owner-authority.js";
|
|
11
|
+
import { mountPaneStreamIssueRoute } from "./pane-stream-issue.js";
|
|
12
|
+
import { projectTerminalRuntimeInventory } from "./resources/terminal-runtime-inventory.js";
|
|
13
|
+
const registrationSchema = TmuxServerRegistrationSchemaZ.omit({ serverId: true }).strict();
|
|
14
|
+
const workspaceSchema = z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(160)
|
|
18
|
+
.refine((value) => !/[\0\r\n]/u.test(value));
|
|
19
|
+
async function boundedJson(request) {
|
|
20
|
+
if (!request.body)
|
|
21
|
+
throw new TypeError("Missing body");
|
|
22
|
+
const reader = request.body.getReader();
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let size = 0;
|
|
25
|
+
try {
|
|
26
|
+
for (;;) {
|
|
27
|
+
const next = await reader.read();
|
|
28
|
+
if (next.done)
|
|
29
|
+
break;
|
|
30
|
+
size += next.value.byteLength;
|
|
31
|
+
if (size > 64 * 1024) {
|
|
32
|
+
void reader.cancel().catch(() => undefined);
|
|
33
|
+
throw new TypeError("Body too large");
|
|
34
|
+
}
|
|
35
|
+
chunks.push(next.value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
reader.releaseLock();
|
|
40
|
+
}
|
|
41
|
+
const bytes = new Uint8Array(size);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const chunk of chunks) {
|
|
44
|
+
bytes.set(chunk, offset);
|
|
45
|
+
offset += chunk.byteLength;
|
|
46
|
+
}
|
|
47
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
48
|
+
}
|
|
49
|
+
function failure(c, error) {
|
|
50
|
+
if (error instanceof TmuxServerScopeError)
|
|
51
|
+
return c.json({ error: { code: error.code } }, error.code === "not-found" ? 404 : error.code === "capacity" ? 429 : 409);
|
|
52
|
+
if (error instanceof WorkspaceMultiplexerError)
|
|
53
|
+
return c.json({ error: { code: error.code } }, 409);
|
|
54
|
+
if (error instanceof z.ZodError || error instanceof SyntaxError || error instanceof TypeError)
|
|
55
|
+
return c.json({ error: { code: "invalid-request" } }, 400);
|
|
56
|
+
return c.json({ error: { code: "server-unavailable" } }, 503);
|
|
57
|
+
}
|
|
58
|
+
/** Owner capability is mandatory even when the ordinary local HTTP surface is open. */
|
|
59
|
+
export function mountTmuxServerRoutes(app, options) {
|
|
60
|
+
const base = TMUX_SERVERS_API_PATH;
|
|
61
|
+
const authorize = async (c, next) => {
|
|
62
|
+
c.header("Cache-Control", "no-store");
|
|
63
|
+
if (!ownerBearerMatches(c.req.header("Authorization"), options.ownerToken))
|
|
64
|
+
return c.json({ error: { code: "owner-required" } }, 401);
|
|
65
|
+
await next();
|
|
66
|
+
};
|
|
67
|
+
app.use(base, authorize);
|
|
68
|
+
app.use(`${base}/*`, authorize);
|
|
69
|
+
const route = (work) => async (c) => {
|
|
70
|
+
try {
|
|
71
|
+
return await work(c);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return failure(c, error);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const scope = (c) => TmuxServerScopeSchemaZ.parse({
|
|
78
|
+
serverId: c.req.param("serverId"),
|
|
79
|
+
generation: c.req.param("generation"),
|
|
80
|
+
});
|
|
81
|
+
app.get(base, route(async (c) => c.json(TmuxServersResourceSchemaZ.parse({ version: 1, servers: await options.owners.refresh() }))));
|
|
82
|
+
app.post(base, route(async (c) => c.json(await options.owners.register(registrationSchema.parse(await boundedJson(c.req.raw))), 201)));
|
|
83
|
+
app.delete(`${base}/:serverId`, route(async (c) => {
|
|
84
|
+
await options.owners.remove(TmuxServerIdSchemaZ.parse(c.req.param("serverId")));
|
|
85
|
+
return c.json({ removed: true });
|
|
86
|
+
}));
|
|
87
|
+
const scoped = `${base}/:serverId/:generation`;
|
|
88
|
+
mountTmuxServerNativeBackingRoute(app, {
|
|
89
|
+
scopedPath: scoped,
|
|
90
|
+
ownerToken: options.ownerToken,
|
|
91
|
+
owners: options.owners,
|
|
92
|
+
});
|
|
93
|
+
app.post(`${scoped}/sessions/create`, route(async (c) => {
|
|
94
|
+
const server = scope(c);
|
|
95
|
+
const request = z
|
|
96
|
+
.object({
|
|
97
|
+
operationId: z.uuid(),
|
|
98
|
+
expectedDaemonInstanceId: z.uuid(),
|
|
99
|
+
intent: WorkspaceSessionCreateArgumentsSchemaZ,
|
|
100
|
+
})
|
|
101
|
+
.strict()
|
|
102
|
+
.parse(await boundedJson(c.req.raw));
|
|
103
|
+
if (request.expectedDaemonInstanceId !== server.generation ||
|
|
104
|
+
(request.intent.expectedDaemonInstanceId &&
|
|
105
|
+
request.intent.expectedDaemonInstanceId !== server.generation))
|
|
106
|
+
return c.json({ error: { code: "stale-generation" } }, 409);
|
|
107
|
+
return c.json(await options.owners.withOwner(server, (owner) => owner.createSession(request.operationId, {
|
|
108
|
+
...request.intent,
|
|
109
|
+
expectedDaemonInstanceId: server.generation,
|
|
110
|
+
})));
|
|
111
|
+
}));
|
|
112
|
+
app.post(`${scoped}/sessions/:liveSessionId/open`, route(async (c) => {
|
|
113
|
+
const server = scope(c);
|
|
114
|
+
const result = await options.owners.withOwner(server, (owner) => owner.openSession(workspaceSchema.parse(c.req.param("liveSessionId"))));
|
|
115
|
+
return c.json({ version: 1, server, ...result });
|
|
116
|
+
}));
|
|
117
|
+
app.get(`${scoped}/sessions`, route(async (c) => {
|
|
118
|
+
const server = scope(c);
|
|
119
|
+
return c.json(TmuxServerSessionsResourceSchemaZ.parse({
|
|
120
|
+
version: 1,
|
|
121
|
+
server,
|
|
122
|
+
sessions: await options.owners.withOwner(server, async (owner) => {
|
|
123
|
+
const sessions = await owner.catalog();
|
|
124
|
+
const workspaces = owner.workspaceRegistry.list();
|
|
125
|
+
return sessions.map((session) => {
|
|
126
|
+
const aliases = workspaces.filter((workspace) => workspace.sessionName === session.sessionName);
|
|
127
|
+
return { ...session, workspaceName: aliases.length === 1 ? aliases[0].name : null };
|
|
128
|
+
});
|
|
129
|
+
}),
|
|
130
|
+
}));
|
|
131
|
+
}));
|
|
132
|
+
app.get(`${scoped}/inventory/:workspaceName`, route(async (c) => {
|
|
133
|
+
const server = scope(c);
|
|
134
|
+
const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
|
|
135
|
+
const resource = await options.owners.withOwner(server, async (owner) => {
|
|
136
|
+
const sessions = await owner.catalog();
|
|
137
|
+
const workspace = owner.workspaceRegistry.get(workspaceName);
|
|
138
|
+
if (!workspace)
|
|
139
|
+
return null;
|
|
140
|
+
const expected = c.req.query("liveSessionId");
|
|
141
|
+
if (expected &&
|
|
142
|
+
!sessions.some((row) => row.sessionName === workspace.sessionName && row.liveSessionId === expected))
|
|
143
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
144
|
+
const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(workspace.sessionName, c.req.raw.signal);
|
|
145
|
+
return snapshot ? projectTerminalRuntimeInventory(snapshot, 0) : null;
|
|
146
|
+
});
|
|
147
|
+
return resource
|
|
148
|
+
? c.json({ version: 1, server, resource })
|
|
149
|
+
: c.json({ error: { code: "workspace-not-found" } }, 404);
|
|
150
|
+
}));
|
|
151
|
+
app.get(`${scoped}/catalog`, route(async (c) => {
|
|
152
|
+
const server = scope(c);
|
|
153
|
+
const resource = await options.owners.withOwner(server, async (owner) => {
|
|
154
|
+
const sessions = await owner.catalog();
|
|
155
|
+
return {
|
|
156
|
+
intents: owner.workspaceRegistry.list().map((entry) => ({
|
|
157
|
+
workspaceName: entry.name,
|
|
158
|
+
sessionName: entry.sessionName,
|
|
159
|
+
source: "workspace",
|
|
160
|
+
availability: sessions.some((row) => row.sessionName === entry.sessionName)
|
|
161
|
+
? "live"
|
|
162
|
+
: "stopped",
|
|
163
|
+
})),
|
|
164
|
+
liveSessions: sessions.map((row) => ({
|
|
165
|
+
sessionName: row.sessionName,
|
|
166
|
+
fleetSessionId: fleetSessionIdForName(row.sessionName),
|
|
167
|
+
paneCount: row.paneCount,
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
return c.json({ version: 1, server, resource });
|
|
172
|
+
}));
|
|
173
|
+
app.get(`${scoped}/application-shell/:workspaceName`, route(async (c) => {
|
|
174
|
+
const server = scope(c);
|
|
175
|
+
const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
|
|
176
|
+
const resource = await options.owners.withOwner(server, async (owner) => {
|
|
177
|
+
const sessions = await owner.catalog();
|
|
178
|
+
const workspace = owner.workspaceRegistry.get(workspaceName);
|
|
179
|
+
if (!workspace)
|
|
180
|
+
return null;
|
|
181
|
+
if (!sessions.some((row) => row.sessionName === workspace.sessionName &&
|
|
182
|
+
row.liveSessionId === c.req.query("liveSessionId")))
|
|
183
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
184
|
+
const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(workspace.sessionName, c.req.raw.signal);
|
|
185
|
+
return snapshot ? projectApplicationShellResource(snapshot) : null;
|
|
186
|
+
});
|
|
187
|
+
return resource
|
|
188
|
+
? c.json({ version: 1, server, resource })
|
|
189
|
+
: c.json({ error: { code: "workspace-not-found" } }, 404);
|
|
190
|
+
}));
|
|
191
|
+
// Subscribe before publishing ready: a topology change during the subsequent HTTP read
|
|
192
|
+
// invalidates that read. This shares the owner's retained mirror, never a new tmux client.
|
|
193
|
+
app.get(`${scoped}/session-events/:workspaceName`, route(async (c) => {
|
|
194
|
+
const server = scope(c);
|
|
195
|
+
const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
|
|
196
|
+
const owner = await options.owners.withOwner(server, async (candidate) => {
|
|
197
|
+
const sessions = await candidate.catalog();
|
|
198
|
+
const workspace = candidate.workspaceRegistry.get(workspaceName);
|
|
199
|
+
if (!workspace ||
|
|
200
|
+
!sessions.some((row) => row.sessionName === workspace.sessionName &&
|
|
201
|
+
row.liveSessionId === c.req.query("liveSessionId")))
|
|
202
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
203
|
+
return candidate;
|
|
204
|
+
});
|
|
205
|
+
const workspace = owner.workspaceRegistry.get(workspaceName);
|
|
206
|
+
return streamSSE(c, async (stream) => {
|
|
207
|
+
let stopped = false;
|
|
208
|
+
let wake = null;
|
|
209
|
+
let revision = 0;
|
|
210
|
+
const dirty = () => {
|
|
211
|
+
revision++;
|
|
212
|
+
wake?.();
|
|
213
|
+
};
|
|
214
|
+
stream.onAbort(() => {
|
|
215
|
+
stopped = true;
|
|
216
|
+
wake?.();
|
|
217
|
+
});
|
|
218
|
+
const identity = await owner.sessionRuntimeRegistry.describeSessionAuthority(workspace.sessionName);
|
|
219
|
+
let topology = null;
|
|
220
|
+
let sessionReplaced = false;
|
|
221
|
+
const subscription = await owner.sessionRuntimeRegistry.subscribeLayout(workspace.sessionName, () => undefined, {
|
|
222
|
+
expectedRuntimeSessionId: identity.runtimeSessionId,
|
|
223
|
+
expectedSemanticPaneIds: identity.description.panes.map((pane) => pane.semanticPaneId),
|
|
224
|
+
onAuthority(snapshot) {
|
|
225
|
+
if (snapshot.windowLinks.liveSessionId !== c.req.query("liveSessionId")) {
|
|
226
|
+
sessionReplaced = true;
|
|
227
|
+
dirty();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Full replacement observes removed windows and retains no historical keys.
|
|
231
|
+
// Geometry, focus and active-link changes stay entirely on the pane stream.
|
|
232
|
+
const signature = JSON.stringify(snapshot.layouts
|
|
233
|
+
.map((layout) => [
|
|
234
|
+
layout.semanticWindowId,
|
|
235
|
+
layout.windowName,
|
|
236
|
+
layout.panes.map((pane) => pane.semanticPaneId).sort(),
|
|
237
|
+
])
|
|
238
|
+
.sort((a, b) => String(a[0]).localeCompare(String(b[0]))));
|
|
239
|
+
if (topology === signature)
|
|
240
|
+
return;
|
|
241
|
+
topology = signature;
|
|
242
|
+
dirty();
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
try {
|
|
246
|
+
options.owners.current(server);
|
|
247
|
+
if (sessionReplaced)
|
|
248
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
249
|
+
await stream.writeSSE({
|
|
250
|
+
data: JSON.stringify({ version: 1, server, type: "ready", revision }),
|
|
251
|
+
});
|
|
252
|
+
let sent = revision;
|
|
253
|
+
let nextIdentityCheck = Date.now() + 5000;
|
|
254
|
+
while (!stopped) {
|
|
255
|
+
await new Promise((resolve) => {
|
|
256
|
+
const timer = setTimeout(resolve, 1000);
|
|
257
|
+
wake = () => {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
resolve();
|
|
260
|
+
};
|
|
261
|
+
if (stopped || revision !== sent)
|
|
262
|
+
wake();
|
|
263
|
+
});
|
|
264
|
+
wake = null;
|
|
265
|
+
if (stopped)
|
|
266
|
+
break;
|
|
267
|
+
if (sessionReplaced)
|
|
268
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
269
|
+
options.owners.current(server);
|
|
270
|
+
if (Date.now() >= nextIdentityCheck) {
|
|
271
|
+
const sessions = await owner.catalog();
|
|
272
|
+
options.owners.current(server);
|
|
273
|
+
if (!sessions.some((row) => row.sessionName === workspace.sessionName &&
|
|
274
|
+
row.liveSessionId === c.req.query("liveSessionId")))
|
|
275
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
276
|
+
nextIdentityCheck = Date.now() + 5000;
|
|
277
|
+
}
|
|
278
|
+
if (revision !== sent) {
|
|
279
|
+
sent = revision;
|
|
280
|
+
await stream.writeSSE({
|
|
281
|
+
data: JSON.stringify({ version: 1, server, type: "invalidate", revision }),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else
|
|
285
|
+
await stream.writeSSE({
|
|
286
|
+
data: JSON.stringify({ version: 1, server, type: "heartbeat", revision }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
if (!stopped)
|
|
292
|
+
await stream
|
|
293
|
+
.writeSSE({ data: JSON.stringify({ version: 1, server, type: "retired", revision }) })
|
|
294
|
+
.catch(() => undefined);
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
await subscription.close();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}));
|
|
301
|
+
app.post(`${scoped}/panes/create`, route(async (c) => {
|
|
302
|
+
const server = scope(c);
|
|
303
|
+
const liveSessionId = workspaceSchema.parse(c.req.query("liveSessionId"));
|
|
304
|
+
const request = WorkspacePaneCreateMutationRequestSchemaZ.parse(await boundedJson(c.req.raw));
|
|
305
|
+
if (request.expectedDaemonInstanceId !== server.generation)
|
|
306
|
+
return c.json({ error: { code: "stale-generation" } }, 409);
|
|
307
|
+
return c.json(await options.owners.withOwner(server, (owner) => owner.createSessionPane(liveSessionId, request)));
|
|
308
|
+
}));
|
|
309
|
+
app.post(`${scoped}/mutations`, route(async (c) => {
|
|
310
|
+
const server = scope(c);
|
|
311
|
+
const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(await boundedJson(c.req.raw));
|
|
312
|
+
if (request.expectedDaemonInstanceId !== server.generation)
|
|
313
|
+
return c.json({ error: { code: "stale-generation" } }, 409);
|
|
314
|
+
const liveSessionId = c.req.query("liveSessionId");
|
|
315
|
+
const result = await options.owners.withOwner(server, (owner) => liveSessionId
|
|
316
|
+
? owner.mutateSession(liveSessionId, request, c.req.header("X-Tmux-Ide-Host-Client-Id"))
|
|
317
|
+
: owner.multiplexerBackend.mutate(request, c.req.header("X-Tmux-Ide-Host-Client-Id"), undefined, true));
|
|
318
|
+
return c.json(result);
|
|
319
|
+
}));
|
|
320
|
+
app.post(`${scoped}/pane-streams/issue`, route(async (c) => {
|
|
321
|
+
const server = scope(c);
|
|
322
|
+
return options.owners.withOwner(server, async (owner) => {
|
|
323
|
+
const sessions = await owner.catalog();
|
|
324
|
+
const liveSessionId = c.req.query("liveSessionId");
|
|
325
|
+
if (liveSessionId) {
|
|
326
|
+
const body = (await boundedJson(c.req.raw.clone()));
|
|
327
|
+
const workspace = owner.workspaceRegistry.get(body.stream?.workspaceName ?? "");
|
|
328
|
+
if (!workspace ||
|
|
329
|
+
!sessions.some((row) => row.sessionName === workspace.sessionName && row.liveSessionId === liveSessionId))
|
|
330
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
331
|
+
}
|
|
332
|
+
const issueApp = new Hono();
|
|
333
|
+
mountPaneStreamIssueRoute(issueApp, {
|
|
334
|
+
daemonInstanceId: server.generation,
|
|
335
|
+
ownerToken: options.ownerToken,
|
|
336
|
+
workspaceRegistry: owner.workspaceRegistry,
|
|
337
|
+
backend: owner.paneStreamRuntime.coordinator,
|
|
338
|
+
});
|
|
339
|
+
const url = new URL(c.req.url);
|
|
340
|
+
url.pathname = PANE_STREAM_ISSUE_PATH;
|
|
341
|
+
url.search = "";
|
|
342
|
+
return await issueApp.request(new Request(url, c.req.raw));
|
|
343
|
+
});
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { captureTmuxServerProof, captureUnboundTmuxSelectorProof, } from "./tmux-server-proof.js";
|
|
2
|
+
import { createPinnedWorkspaceTmuxRunner, resolveWorkspacePaneTmuxAuthority, } from "./workspace-pane-creation.js";
|
|
3
|
+
import { resolveRuntimeNamespace, resolveTmuxServerIntent } from "./runtime-namespace.js";
|
|
2
4
|
import { compareProductVersions } from "./semver.js";
|
|
3
5
|
import { spawn } from "node:child_process";
|
|
4
6
|
import { resolve } from "node:path";
|
|
@@ -64,6 +66,16 @@ function sameCanonicalInstance(left, right) {
|
|
|
64
66
|
const defaultDependencies = {
|
|
65
67
|
// This adapter owns startup, so legacy permission preparation is explicit
|
|
66
68
|
// here. Injected inspectors remain isolated from real filesystem mutation.
|
|
69
|
+
serverProof: (intent) => {
|
|
70
|
+
try {
|
|
71
|
+
const authority = { ...resolveWorkspacePaneTmuxAuthority(), socketSelector: intent.selector };
|
|
72
|
+
return (captureTmuxServerProof(createPinnedWorkspaceTmuxRunner(authority, { timeoutMs: 1000 })) ??
|
|
73
|
+
captureUnboundTmuxSelectorProof(authority));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
67
79
|
inspect: prepareCanonicalDaemonInfoForBootstrap,
|
|
68
80
|
ownerProvenDead: isCanonicalDaemonRecordOwnerProvenDead,
|
|
69
81
|
alive: isCanonicalDaemonAlive,
|
|
@@ -88,11 +100,14 @@ function needsReplacement(info, expected) {
|
|
|
88
100
|
}
|
|
89
101
|
return info.protocolVersion < DAEMON_WIRE_PROTOCOL_VERSION;
|
|
90
102
|
}
|
|
91
|
-
async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProductVersion) {
|
|
103
|
+
async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProductVersion, intent = null) {
|
|
92
104
|
if (!needsReplacement(info, expectedProductVersion)) {
|
|
93
105
|
throw new DaemonBootstrapError("incompatible", `Canonical daemon protocol ${info.protocolVersion} cannot be replaced by older protocol ${DAEMON_WIRE_PROTOCOL_VERSION}.`, { reason: "protocol-mismatch" });
|
|
94
106
|
}
|
|
95
|
-
const [identity, health] = await Promise.all([
|
|
107
|
+
const [identity, health] = await Promise.all([
|
|
108
|
+
deps.identity(info, undefined, Boolean(intent)),
|
|
109
|
+
deps.health(info),
|
|
110
|
+
]);
|
|
96
111
|
if (!info.authToken ||
|
|
97
112
|
!identity ||
|
|
98
113
|
!health ||
|
|
@@ -105,6 +120,7 @@ async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProduc
|
|
|
105
120
|
health.productVersion !== info.productVersion) {
|
|
106
121
|
throw new DaemonBootstrapError("incompatible", "The older canonical daemon changed identity before the version upgrade.", { reason: "identity-mismatch" });
|
|
107
122
|
}
|
|
123
|
+
assertServerIntent(deps, info, identity, intent);
|
|
108
124
|
const latest = deps.inspect();
|
|
109
125
|
if (latest.status !== "valid" ||
|
|
110
126
|
!sameCanonicalInstance(latest.info, info) ||
|
|
@@ -131,7 +147,17 @@ async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProduc
|
|
|
131
147
|
}
|
|
132
148
|
throw new DaemonBootstrapError("control-timeout", "The older canonical daemon did not retire after accepting the version upgrade.", { reason: "protocol-mismatch" });
|
|
133
149
|
}
|
|
134
|
-
|
|
150
|
+
function assertServerIntent(deps, info, identity, intent) {
|
|
151
|
+
if (!intent)
|
|
152
|
+
return;
|
|
153
|
+
const proof = deps.serverProof(intent);
|
|
154
|
+
if (info.tmuxServerProofVersion !== 1 || !identity.tmuxServerProof || !proof)
|
|
155
|
+
throw new DaemonBootstrapError("incompatible", "Cannot prove the requested tmux server for this daemon. Its server identity is unavailable or unsupported; the existing daemon was left running.", { reason: "tmux-server-unproven" });
|
|
156
|
+
if (identity.tmuxServerProof.kind !== proof.kind ||
|
|
157
|
+
identity.tmuxServerProof.digest !== proof.digest)
|
|
158
|
+
throw new DaemonBootstrapError("incompatible", "The existing daemon manages a different tmux server. It was left running; select its server or use a separate daemon namespace.", { reason: "tmux-server-mismatch" });
|
|
159
|
+
}
|
|
160
|
+
async function probeCanonical(deps, expectedProductVersion, intent = null) {
|
|
135
161
|
const state = deps.inspect();
|
|
136
162
|
if (state.status === "missing")
|
|
137
163
|
return { status: "absent-or-stale" };
|
|
@@ -149,7 +175,7 @@ async function probeCanonical(deps, expectedProductVersion) {
|
|
|
149
175
|
if (!(await deps.alive(state.info)))
|
|
150
176
|
return { status: state.info.supervisionId ? "owner-pending" : "absent-or-stale" };
|
|
151
177
|
const [identity, health] = await Promise.all([
|
|
152
|
-
deps.identity(state.info),
|
|
178
|
+
deps.identity(state.info, undefined, Boolean(intent)),
|
|
153
179
|
deps.health(state.info),
|
|
154
180
|
]);
|
|
155
181
|
// A living elected generation may publish before its accept loop. Preserve
|
|
@@ -162,6 +188,7 @@ async function probeCanonical(deps, expectedProductVersion) {
|
|
|
162
188
|
identity.startedAt !== state.info.startedAt) {
|
|
163
189
|
return { status: "incompatible", reason: "identity-mismatch" };
|
|
164
190
|
}
|
|
191
|
+
assertServerIntent(deps, state.info, identity, intent);
|
|
165
192
|
if (state.info.protocolVersion !== DAEMON_WIRE_PROTOCOL_VERSION ||
|
|
166
193
|
identity.protocolVersion !== state.info.protocolVersion ||
|
|
167
194
|
health.protocolVersion !== state.info.protocolVersion) {
|
|
@@ -211,9 +238,15 @@ function supervisedAdmission(deps, declaredBinding) {
|
|
|
211
238
|
};
|
|
212
239
|
}
|
|
213
240
|
export function createCanonicalDaemonBootstrapCoordinator(options, dependencies = {}) {
|
|
241
|
+
options = {
|
|
242
|
+
...options,
|
|
243
|
+
tmuxServerIntent: options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent,
|
|
244
|
+
};
|
|
214
245
|
const deps = supervisedAdmission({ ...defaultDependencies, ...dependencies }, options.supervisionId);
|
|
215
246
|
return new DaemonBootstrapCoordinator({
|
|
216
|
-
probe: () => probeCanonical(deps, options.expectedProductVersion
|
|
247
|
+
probe: () => probeCanonical(deps, options.expectedProductVersion, options.tmuxServerIntent === undefined
|
|
248
|
+
? resolveTmuxServerIntent()
|
|
249
|
+
: options.tmuxServerIntent),
|
|
217
250
|
spawn: () => deps.spawnOwner(resolve(options.entryPath), resolve(options.cwd ?? process.cwd())),
|
|
218
251
|
timeoutMs: options.timeoutMs,
|
|
219
252
|
onPhaseChanged: options.onPhaseChanged,
|
|
@@ -222,6 +255,10 @@ export function createCanonicalDaemonBootstrapCoordinator(options, dependencies
|
|
|
222
255
|
});
|
|
223
256
|
}
|
|
224
257
|
export function ensureCanonicalDaemon(options, dependencies = {}) {
|
|
258
|
+
options = {
|
|
259
|
+
...options,
|
|
260
|
+
tmuxServerIntent: options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent,
|
|
261
|
+
};
|
|
225
262
|
const deps = supervisedAdmission({ ...defaultDependencies, ...dependencies }, options.supervisionId);
|
|
226
263
|
const ensure = () => createCanonicalDaemonBootstrapCoordinator(options, deps).ensure();
|
|
227
264
|
return ensure().catch(async (error) => {
|
|
@@ -246,7 +283,9 @@ export function ensureCanonicalDaemon(options, dependencies = {}) {
|
|
|
246
283
|
}
|
|
247
284
|
const replacing = state.info;
|
|
248
285
|
try {
|
|
249
|
-
await replaceOlderCanonicalDaemon(deps, replacing, options.timeoutMs ?? 15_000, options.expectedProductVersion
|
|
286
|
+
await replaceOlderCanonicalDaemon(deps, replacing, options.timeoutMs ?? 15_000, options.expectedProductVersion, options.tmuxServerIntent === undefined
|
|
287
|
+
? resolveTmuxServerIntent()
|
|
288
|
+
: options.tmuxServerIntent);
|
|
250
289
|
}
|
|
251
290
|
catch (replacementError) {
|
|
252
291
|
// A duplicate upgrader can retire the exact owner while this caller is
|
|
@@ -265,6 +304,10 @@ export function ensureCanonicalDaemon(options, dependencies = {}) {
|
|
|
265
304
|
}
|
|
266
305
|
/** Retire only a verified older owner; the foreground caller retains ownership of startup. */
|
|
267
306
|
export async function retireOutdatedCanonicalDaemon(options, dependencies = {}) {
|
|
307
|
+
options = {
|
|
308
|
+
...options,
|
|
309
|
+
tmuxServerIntent: options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent,
|
|
310
|
+
};
|
|
268
311
|
const deps = supervisedAdmission({ ...defaultDependencies, ...dependencies }, options.supervisionId);
|
|
269
312
|
const state = deps.inspect();
|
|
270
313
|
if (options.supervisionId &&
|
|
@@ -283,7 +326,7 @@ export async function retireOutdatedCanonicalDaemon(options, dependencies = {})
|
|
|
283
326
|
return false;
|
|
284
327
|
}
|
|
285
328
|
try {
|
|
286
|
-
await replaceOlderCanonicalDaemon(deps, state.info, options.timeoutMs ?? 15_000, options.expectedProductVersion);
|
|
329
|
+
await replaceOlderCanonicalDaemon(deps, state.info, options.timeoutMs ?? 15_000, options.expectedProductVersion, options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent);
|
|
287
330
|
return true;
|
|
288
331
|
}
|
|
289
332
|
catch (error) {
|
|
@@ -294,3 +337,16 @@ export async function retireOutdatedCanonicalDaemon(options, dependencies = {})
|
|
|
294
337
|
throw error;
|
|
295
338
|
}
|
|
296
339
|
}
|
|
340
|
+
/** Admission for foreground election winners and explicit takeover paths. */
|
|
341
|
+
export async function assertCanonicalDaemonServerIntent(info, intent = resolveTmuxServerIntent(), dependencies = {}) {
|
|
342
|
+
if (!intent)
|
|
343
|
+
return;
|
|
344
|
+
const deps = { ...defaultDependencies, ...dependencies };
|
|
345
|
+
const identity = await deps.identity(info, undefined, true);
|
|
346
|
+
if (!identity ||
|
|
347
|
+
identity.pid !== info.pid ||
|
|
348
|
+
identity.instanceId !== info.instanceId ||
|
|
349
|
+
identity.startedAt !== info.startedAt)
|
|
350
|
+
throw new DaemonBootstrapError("incompatible", "Canonical daemon identity could not be verified for the requested tmux server.", { reason: "identity-mismatch" });
|
|
351
|
+
assertServerIntent(deps, info, identity, intent);
|
|
352
|
+
}
|
|
@@ -432,6 +432,9 @@ export function writeCanonicalDaemonInfo(info, claim) {
|
|
|
432
432
|
instanceId: info.instanceId,
|
|
433
433
|
startedAt: info.startedAt,
|
|
434
434
|
...(info.environmentId !== undefined ? { environmentId: info.environmentId } : {}),
|
|
435
|
+
...(info.tmuxServerProofVersion !== undefined
|
|
436
|
+
? { tmuxServerProofVersion: info.tmuxServerProofVersion }
|
|
437
|
+
: {}),
|
|
435
438
|
bindHostname: info.bindHostname,
|
|
436
439
|
authToken: info.authToken,
|
|
437
440
|
...(info.provenance !== undefined ? { provenance: info.provenance } : {}),
|
|
@@ -795,11 +798,11 @@ export async function probeCanonicalDaemonHealth(info, parentSignal) {
|
|
|
795
798
|
}
|
|
796
799
|
}
|
|
797
800
|
/** Credential-free endpoint binding probe; callers compare instanceId to daemon.json. */
|
|
798
|
-
export async function probeCanonicalDaemonIdentity(info, parentSignal) {
|
|
801
|
+
export async function probeCanonicalDaemonIdentity(info, parentSignal, includeTmuxServerProof = false) {
|
|
799
802
|
if (!(await isCanonicalDaemonAlive(info)))
|
|
800
803
|
return null;
|
|
801
804
|
try {
|
|
802
|
-
const res = await fetch(canonicalDaemonUrl("http", info.bindHostname, info.port, "/identity"), {
|
|
805
|
+
const res = await fetch(canonicalDaemonUrl("http", info.bindHostname, info.port, includeTmuxServerProof ? "/identity?tmuxServerProof=1" : "/identity"), {
|
|
803
806
|
signal: parentSignal ?? timeoutSignal(750),
|
|
804
807
|
});
|
|
805
808
|
if (!res.ok)
|